Last active
April 11, 2021 22:34
-
-
Save Pzixel/fe2a8d42e12d769b508e608c0aaf15a5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
pragma solidity ^0.5.2; | |
contract Owned { | |
address public owner; | |
constructor() public { | |
owner = tx.origin; | |
} | |
modifier onlyOwner { | |
require(msg.sender == owner); | |
_; | |
} | |
modifier onlyOrigin { | |
require(tx.origin == owner); | |
_; | |
} | |
function isDeployed() public pure returns (bool) { | |
return true; | |
} | |
} | |
contract ISeason is Owned { | |
function getHistoricalIndices() public view returns (uint64[] memory); | |
function getDistributionRequestByIndex(uint64 index) public view returns (bytes30 serviceNumber, Types.DeclarantType, uint64[] memory fairIds, Types.Speciality, uint64[] memory periods, bytes16, uint64[] memory periodStatusCodes); | |
function getStatusUpdates(bytes30) public view returns (uint64[] memory, uint64[] memory, string memory); | |
} | |
contract IDistributor is Owned { | |
function isLoaded() public view returns (bool); | |
function isDistributed() public view returns (bool); | |
function loadRequests() public; | |
function distribute() public; | |
function getPeriodsCount() public view returns(uint64); | |
function getPeriod(uint64 index) public view returns(uint64, uint64, bytes30[] memory, bytes30[] memory, bytes30[] memory, Types.Speciality); | |
function init(uint64[] memory fairsIds, uint64[] memory periods, Types.Speciality[] memory specialities, uint64[] memory placesCount) public; | |
function getPlaces() public view returns(uint64[] memory total, uint64[] memory unoccupied, bool[] memory areUpdatable); | |
function getRequestedPlaces() public view returns(uint64[] memory); | |
function updatePlaces(uint64[] memory placesCounts) public; | |
function finalize(Types.DistributionType distributionType) public; | |
function tryApprove(uint64 fairId, uint64 date, Types.Speciality speciality, bytes30[] memory serviceNumbers) public; | |
} | |
// Фабрика, хранящая в себе список сезонов, их начала и распределения | |
contract SeasonFactory is Owned { | |
address[] public seasons; | |
address[] public distributions; | |
address public newVersionAddress; | |
uint64[] seasonPeriodsBegins; | |
uint64[] seasonBegins; | |
uint64[] seasonEnds; | |
event SeasonCreated(uint64 indexed begin, uint64 indexed end, address season); | |
// Миграция на новую версию, вызывается из Debug проекта | |
function migrateToNewVersion(address newVersionAddress_) public onlyOwner { | |
require(newVersionAddress == address(0)); | |
require(newVersionAddress_ != address(this)); | |
SeasonFactory newVersion = SeasonFactory(newVersionAddress_); | |
require(newVersion.owner() == owner); | |
require(newVersion.isDeployed()); | |
newVersionAddress = newVersionAddress_; | |
} | |
// Метод получения всей информации по сезонам | |
function getSeasons() public view returns (address[] memory seasonAddresses, uint64[] memory seasonBegins_, uint64[] memory seasonEnds_, uint64[] memory seasonPeriodBegins_) { | |
if (newVersionAddress != address(0)) { | |
SeasonFactory newVersion = SeasonFactory(newVersionAddress); | |
return newVersion.getSeasons(); | |
} | |
return (seasons, seasonBegins, seasonEnds, seasonPeriodsBegins); | |
} | |
// Метод добавления сезона. Вызывается автоматически из C# кода после создания сезона (см. метод SeasonFactory.CreateSeasonAsync) | |
function addSeason(address seasonAddress, uint64 seasonBegin, uint64 seasonEnd, uint64 seasonPeriodsBegin) public onlyOwner { | |
if (newVersionAddress != address(0)) { | |
SeasonFactory newVersion = SeasonFactory(newVersionAddress); | |
newVersion.addSeason(seasonAddress, seasonBegin, seasonEnd, seasonPeriodsBegin); | |
return; | |
} | |
ISeason season = ISeason(seasonAddress); | |
require(season.owner() == owner); | |
seasons.push(seasonAddress); | |
seasonBegins.push(seasonBegin); | |
seasonEnds.push(seasonEnd); | |
seasonPeriodsBegins.push(seasonPeriodsBegin); | |
emit SeasonCreated(seasonBegin, seasonEnd, seasonAddress); // Эмиттит эвент создания сезона. По-хорошему нужно его отслеживать и перезапускать приложение, когда оно появилось, чтобы обновить список сезонов | |
} | |
// Добавлет распределение. Вызываеся автоматически при вызове запуска распределения через DistributionFacade | |
function addDistribution(address distributionManagerAddress) public onlyOwner { | |
if (newVersionAddress != address(0)) { | |
SeasonFactory newVersion = SeasonFactory(newVersionAddress); | |
newVersion.addDistribution(distributionManagerAddress); | |
return; | |
} | |
Owned distributionManager = Owned(distributionManagerAddress); | |
require(distributionManager.owner() == owner); | |
distributions.push(distributionManagerAddress); | |
} | |
function getDistributionsCount() public view returns (uint64) { | |
if (newVersionAddress != address(0)) { | |
SeasonFactory newVersion = SeasonFactory(newVersionAddress); | |
return newVersion.getDistributionsCount(); | |
} | |
return uint64(distributions.length); | |
} | |
function getSeasonsCount() public view returns (uint64) { | |
if (newVersionAddress != address(0)) { | |
SeasonFactory newVersion = SeasonFactory(newVersionAddress); | |
return newVersion.getSeasonsCount(); | |
} | |
return uint64(seasons.length); | |
} | |
// Метод получения сезона по одному из периодов (любому), возвращает первый сезон у которого дата начала предшествует данной дате. Поэтому все сезоны должны идти по порядку от старых к новым | |
function getSeasonForPeriod(uint64 period) public view returns (address) { | |
if (newVersionAddress != address(0)) { | |
SeasonFactory newVersion = SeasonFactory(newVersionAddress); | |
return newVersion.getSeasonForPeriod(period); | |
} | |
if (seasons.length == 0) { | |
return address(0); | |
} | |
for (uint i = seasons.length - 1; ; i--) { | |
if (seasonPeriodsBegins[i] <= period) { | |
return seasons[i]; | |
} | |
if (i == 0) { | |
return address(0); | |
} | |
} | |
} | |
function getLastSeason() public view returns (address) { | |
if (newVersionAddress != address(0)) { | |
SeasonFactory newVersion = SeasonFactory(newVersionAddress); | |
return newVersion.getLastSeason(); | |
} | |
if (seasons.length == 0) { | |
return address(0); | |
} | |
return seasons[seasons.length - 1]; | |
} | |
} | |
// Контракт сезона с заявками | |
contract Season is Owned { | |
string public name; | |
uint64 requestCount; | |
Node[] nodes; | |
uint64 headIndex; | |
uint64 tailIndex; | |
mapping(bytes30 => uint64) requestServiceNumberToIndex; | |
event RequestCreated(bytes30 indexed serviceNumber, uint64 index); | |
constructor(string memory name_) public { | |
name = name_; | |
} | |
// Метод добавления заявки. Имеет максиально допустимое количество переменных, что обуславливает необходимость функций вроде `validateServiceNumber` - если их заинлайнить, компилятор не сможет собрать код. | |
function createRequest(bytes30 serviceNumber, uint64 date, uint64 regNum, Types.DeclarantType declarantType, string memory declarantName, uint64 fairId, Types.Speciality speciality, uint64 district, uint64 region, string memory details, uint64[] memory periods, bytes16 userId) public onlyOwner { | |
validateServiceNumber(serviceNumber); | |
nodes.length++; | |
uint64 newlyInsertedIndex = getRequestsCount() - 1; | |
uint128 dateRegNumPair = (uint128(date) << 64) | uint128(regNum); // упаковываем два числа в одно удвоенного размера, чтобы сортировать по нему заявки. В итоге дает сортировку вида OrderBy(x=>x.date).ThenBy(x=>x.RegNum) | |
Request storage request = nodes[newlyInsertedIndex].request; | |
request.serviceNumber = serviceNumber; | |
request.date = date; | |
request.regNum = regNum; | |
request.dateRegNumPair = dateRegNumPair; | |
request.declarantType = declarantType; | |
request.declarantName = declarantName; | |
request.primaryFairId = fairId; | |
request.currentFairId = fairId; | |
request.fairIds.push(fairId); | |
request.district = district; | |
request.region = region; | |
request.speciality = speciality; | |
request.details = details; | |
request.userId = userId; | |
requestServiceNumberToIndex[request.serviceNumber] = newlyInsertedIndex; | |
setInitialPeriods(request, periods, fairId); | |
fixPlacementInHistory(newlyInsertedIndex, dateRegNumPair); // Обновляет связный список индексов заявок, чтобы поддерживать историчность. | |
emit RequestCreated(serviceNumber, newlyInsertedIndex); // событие добавления заявки, которую регистрирует индексер | |
} | |
// Обработчик сообщения ChangeDeclarantTypeToFarmerMessage из реббита | |
function setRequestDeclarantTypeToFarmer(bytes30 serviceNumber) public onlyOwner { | |
require(!isNewRequest(serviceNumber), "Request with provided service number was not found"); | |
int index = getRequestIndex(serviceNumber); | |
Request storage request = nodes[uint64(index)].request; | |
request.declarantType = Types.DeclarantType.Farmer; | |
} | |
// Обработчик сообщения RequestPeriodsStatusUpdate из реббита, заменяет все периоды на переданные | |
function setRequestPeriods(bytes30 serviceNumber, uint64 statusCode, uint64 responseDate, uint64[] memory fairIds, uint64[] memory periods, uint64[] memory periodStatusCodes, string memory details) public onlyOwner { | |
require(!isNewRequest(serviceNumber), "Request with provided service number was not found"); | |
int index = getRequestIndex(serviceNumber); | |
Request storage request = nodes[uint64(index)].request; | |
bool hasChangedFair = updateCurrentFairIfNew(request, fairIds); | |
uint previousPeriodsLength = request.periods.length; | |
for (uint i = 0; i < periods.length; i++) { | |
uint64 date = periods[i]; | |
uint64 periodStatusCode = periodStatusCodes[i]; | |
uint64 fairId = fairIds[i]; | |
if (hasChangedFair && fairIds[i] == request.currentFairId) { | |
request.periods.push(StatusPeriod(date, periodStatusCode, fairId)); | |
} else { | |
for (uint j = 0; j < previousPeriodsLength; j++) { // we're ok with O(N^2) here | |
StatusPeriod storage period = request.periods[j]; | |
if (period.date == date && period.fairId == fairId) { | |
period.status = periodStatusCode; | |
break; | |
} | |
} | |
} | |
} | |
updateStatusInternal(request, responseDate, statusCode, ""); | |
request.details = details; | |
} | |
function updateCurrentFairIfNew(Request storage request, uint64[] memory fairIds) private returns(bool) { | |
Types.OptionU64 memory newFairId = Types.OptionU64(false, 0); | |
for (uint i = 0; i < fairIds.length; i++) { | |
uint64 fairId = fairIds[i]; | |
if (!containsU64(request.fairIds, fairId)) { | |
require (!newFairId.hasValue || fairId == newFairId.value); | |
newFairId = Types.OptionU64(true, fairId); | |
} | |
} | |
if (newFairId.hasValue) { | |
request.fairIds.push(newFairId.value); | |
request.currentFairId = newFairId.value; | |
return true; | |
} | |
return false; | |
} | |
// Идем с конца списка, ищем, куда вставить элемент, обновляем соответствующие индексы | |
function fixPlacementInHistory(uint64 newlyInsertedIndex, uint128 dateRegNumPair) private onlyOwner { | |
if (newlyInsertedIndex == 0) { | |
return; | |
} | |
Types.OptionU64 memory currentIndex = Types.OptionU64(true, tailIndex); | |
while (currentIndex.hasValue) { | |
Node storage n = nodes[currentIndex.value]; | |
if (n.request.dateRegNumPair <= dateRegNumPair) { | |
break; | |
} | |
currentIndex = n.prev; | |
} | |
if (!currentIndex.hasValue) { | |
nodes[headIndex].prev = Types.OptionU64(true, newlyInsertedIndex); | |
nodes[newlyInsertedIndex].next = Types.OptionU64(true, headIndex); | |
headIndex = newlyInsertedIndex; | |
} | |
else { | |
Node storage currentNode = nodes[currentIndex.value]; | |
Node storage newNode = nodes[newlyInsertedIndex]; | |
newNode.prev = currentIndex; | |
newNode.next = currentNode.next; | |
if (currentNode.next.hasValue) { | |
nodes[currentNode.next.value].prev = Types.OptionU64(true, newlyInsertedIndex); | |
} else if (currentIndex.value == tailIndex) { | |
tailIndex = newlyInsertedIndex; | |
} | |
currentNode.next = Types.OptionU64(true, newlyInsertedIndex); | |
} | |
} | |
// Обработчик сообщения UpdateStatus из ЕТП | |
function updateStatus(bytes30 serviceNumber, uint64 responseDate, uint64 statusCode, string memory note) public onlyOwner { | |
require(!isNewRequest(serviceNumber), "Request with provided service number was not found"); | |
require(isNewStatus(serviceNumber, responseDate, statusCode, note), "Duplicate periodStatusCodes are not allowed"); | |
int index = getRequestIndex(serviceNumber); | |
Request storage request = nodes[uint64(index)].request; | |
for (uint i = 0; i < request.periods.length; i++) { | |
request.periods[i].status = statusCode; | |
} | |
updateStatusInternal(request, responseDate, statusCode, note); | |
} | |
// Проверка статус на дубликат, используется для require и зеркальной проверки в C#, см. использование StatusAlreadyExistsException | |
function isNewStatus(bytes30 serviceNumber, uint64 responseDate, uint64 statusCode, string memory note) public view returns(bool) { | |
int index = getRequestIndex(serviceNumber); | |
Request storage request = nodes[uint64(index)].request; | |
for (uint64 i = 0; i < request.statusUpdates.length; i++) { | |
Types.StatusUpdate storage update = request.statusUpdates[i]; | |
if ( | |
update.responseDate == responseDate | |
&& update.statusCode == statusCode | |
&& bytes(update.note).length == bytes(note).length && containsString(update.note, note) | |
) { | |
return false; | |
} | |
} | |
return true; | |
} | |
function updateStatusInternal(Request storage request, uint64 responseDate, uint64 statusCode, string memory note) private { | |
request.statusUpdates.push(Types.StatusUpdate(responseDate, statusCode, note)); | |
request.statusUpdatesNotes = strConcat(request.statusUpdatesNotes, "\x1f", note); | |
} | |
// Раньше возвращало даты начала и конца, но из-за старых сломанных сезонов эта информация переехала в фабрику, а в сезоне хранится только его имя | |
function getSeasonDetails() public view returns (uint64, uint64, string memory name_) { | |
return (0, 0, name); | |
} | |
// Список всех serviceNumber в порядке добавления в блокчейн. Сейчас используется только в debug-проекте | |
function getAllServiceNumbers() public view returns (bytes30[] memory) { | |
bytes30[] memory result = new bytes30[](getRequestsCount()); | |
for (uint64 i = 0; i < result.length; i++) { | |
result[i] = nodes[i].request.serviceNumber; | |
} | |
return result; | |
} | |
// Возвращает перечень индексов заявок в порядке историчности, по сути просто обходим наш список индексов (который поддерживает функция fixPlacementInHistory) и возвращает эти индексы. | |
// Затем заявку можно получить по методу getRequestByIndex, и таким образом получить список всех заявок в правильном порядке (более ранние заявки - первые) | |
function getHistoricalIndices() public view returns (uint64[] memory){ | |
uint64[] memory result = new uint64[](getRequestsCount()); | |
Types.OptionU64 memory currentIndex = Types.OptionU64(true, headIndex); | |
for (uint64 i = 0; i < nodes.length; i++) { | |
require(currentIndex.hasValue); | |
Node storage node = nodes[currentIndex.value]; | |
result[i] = currentIndex.value; | |
currentIndex = node.next; | |
} | |
return result; | |
} | |
function getRequestIndex(bytes30 serviceNumber) public view returns (int) { | |
uint64 index = requestServiceNumberToIndex[serviceNumber]; | |
if (index == 0 && (nodes.length == 0 || nodes[0].request.serviceNumber != serviceNumber)) { | |
return - 1; | |
} | |
return int(index); | |
} | |
function getRequestByServiceNumber(bytes30 serviceNumber) public view returns (bytes30, uint64, Types.DeclarantType, string memory, uint64, Types.Speciality, uint64, uint64, string memory, uint64[] memory, bytes16, uint64) { | |
int index = getRequestIndex(serviceNumber); | |
if (index < 0) { | |
return (0, 0, Types.DeclarantType.Individual, "", 0, Types.Speciality.UNUSED, 0, 0, "", new uint64[](0), 0, 0); | |
} | |
return getRequestByIndex(uint64(index)); | |
} | |
// Метод получения заявки для фронт-енда | |
function getRequestByIndex(uint64 index) public view returns (bytes30, uint64, Types.DeclarantType, string memory, uint64, Types.Speciality, uint64, uint64, string memory, uint64[] memory periods, bytes16, uint64) { | |
Request storage request = nodes[index].request; | |
(periods, , ) = splitPeriods(request.periods); | |
return (request.serviceNumber, request.date, request.declarantType, request.declarantName, request.currentFairId, request.speciality, request.district, request.region, request.details, periods, request.userId, request.regNum); | |
} | |
// Метод получения заявки для распределения. Раньше хватало одного метода для возврата всех данных, но с добавлением массива ярмарок количество переменных перевалило за то, которое солидити может переживать, и пришлось написать еще один метол | |
function getDistributionRequestByIndex(uint64 index) public view returns (bytes30 serviceNumber, Types.DeclarantType, uint64[] memory fairIds, Types.Speciality, uint64[] memory periods, bytes16, uint64[] memory periodStatusCodes) { | |
Request storage request = nodes[index].request; | |
(periods, periodStatusCodes, fairIds) = splitPeriods(request.periods); | |
return (request.serviceNumber, request.declarantType, fairIds, request.speciality, periods, request.userId, periodStatusCodes); | |
} | |
// Разделяет массив структур на структуру массивов (AoS => SoA) | |
function splitPeriods(StatusPeriod[] memory periods) private pure returns (uint64[] memory dates_, uint64[] memory periodStatusCodes_, uint64[] memory fairs_) { | |
uint64[] memory dates = new uint64[](periods.length); | |
uint64[] memory periodStatusCodes = new uint64[](periods.length); | |
uint64[] memory fairs = new uint64[](periods.length); | |
for (uint i = 0; i < periods.length; i++) { | |
dates[i] = periods[i].date; | |
periodStatusCodes[i] = periods[i].status; | |
fairs[i] = periods[i].fairId; | |
} | |
return (dates, periodStatusCodes, fairs); | |
} | |
function getRequestsCount() public view returns (uint64) { | |
return uint64(nodes.length); | |
} | |
function getStatusUpdates(bytes30 serviceNumber) public view returns (uint64[] memory, uint64[] memory, string memory) { | |
int index = getRequestIndex(serviceNumber); | |
if (index < 0) { | |
return (new uint64[](0), new uint64[](0), ""); | |
} | |
Request storage request = nodes[uint64(index)].request; | |
uint64[] memory dates = new uint64[](request.statusUpdates.length); | |
uint64[] memory statusCodes = new uint64[](request.statusUpdates.length); | |
for (uint64 i = 0; i < request.statusUpdates.length; i++) { | |
dates[i] = request.statusUpdates[i].responseDate; | |
statusCodes[i] = request.statusUpdates[i].statusCode; | |
} | |
return (dates, statusCodes, request.statusUpdatesNotes); | |
} | |
// Старый метод поиска по блокчейну, ныне не используется нигде кроме тестов. | |
function getMatchingRequests(uint64 skipCount, uint64 takeCount, Types.DeclarantType[] memory declarantTypes, string memory declarantName, uint64 fairId, Types.Speciality speciality, uint64 district) public view returns (uint64[] memory, uint64) { | |
uint64[] memory result = new uint64[](takeCount); | |
uint64 skippedCount = 0; | |
uint64 tookCount = 0; | |
Types.OptionU64 memory currentIndex = Types.OptionU64(true, headIndex); | |
for (uint64 i = 0; i < nodes.length && tookCount < result.length; i++) { | |
require(currentIndex.hasValue); | |
Node storage node = nodes[currentIndex.value]; | |
if (isMatch(node.request, declarantTypes, declarantName, fairId, speciality, district)) { | |
if (skippedCount < skipCount) { | |
skippedCount++; | |
} | |
else { | |
result[tookCount++] = currentIndex.value; | |
} | |
} | |
currentIndex = node.next; | |
} | |
return (result, tookCount); | |
} | |
function isMatch(Request memory request, Types.DeclarantType[] memory declarantTypes, string memory declarantName_, uint64 fairId_, Types.Speciality speciality_, uint64 district_) private pure returns (bool) { | |
if (declarantTypes.length != 0 && !containsDeclarant(declarantTypes, request.declarantType)) { | |
return false; | |
} | |
if (!isEmpty(declarantName_) && !containsString(request.declarantName, declarantName_)) { | |
return false; | |
} | |
if (fairId_ != 0 && fairId_ != request.primaryFairId) { | |
return false; | |
} | |
if (district_ != 0 && district_ != request.district) { | |
return false; | |
} | |
if (speciality_ != Types.Speciality.UNUSED && speciality_ != request.speciality) { | |
return false; | |
} | |
return true; | |
} | |
function containsDeclarant(Types.DeclarantType[] memory array, Types.DeclarantType value) private pure returns (bool) { | |
for (uint i = 0; i < array.length; i++) { | |
if (array[i] == value) | |
return true; | |
} | |
return false; | |
} | |
function containsU64(uint64[] memory array, uint64 value) private pure returns (bool) { | |
for (uint i = 0; i < array.length; i++) { | |
if (array[i] == value) | |
return true; | |
} | |
return false; | |
} | |
// Проверка на дубликат заявки. см. также RequestAlreadyExistsException | |
function validateServiceNumber(bytes30 serviceNumber) private view { | |
require(isNewRequest(serviceNumber), "Request with provided service number already exists"); | |
} | |
// Проверка на существование заявки. см. также RequestNotFoundException | |
function isNewRequest(bytes30 serviceNumber) public view returns(bool) { | |
return getRequestIndex(serviceNumber) < 0; | |
} | |
// Первый статус заявки 1010. Мы отбрасываем эти сообщения на уровне деперсонификатора, поэтому нужно восстановить эту информацию в контракте | |
function setInitialPeriods(Request storage request, uint64[] memory periods, uint64 fairId) private { | |
request.statusUpdates.push(Types.StatusUpdate(request.date, 1010, "")); | |
for (uint i = 0; i < periods.length; i++) { | |
request.periods.push(StatusPeriod(periods[i], 1010, fairId)); | |
} | |
} | |
function isEmpty(string memory value) private pure returns (bool) { | |
return bytes(value).length == 0; | |
} | |
function containsString(string memory _base, string memory _value) internal pure returns (bool) { | |
bytes memory _baseBytes = bytes(_base); | |
bytes memory _valueBytes = bytes(_value); | |
if (_baseBytes.length < _valueBytes.length) { | |
return false; | |
} | |
for (uint j = 0; j <= _baseBytes.length - _valueBytes.length; j++) { | |
uint i = 0; | |
for (; i < _valueBytes.length; i++) { | |
if (_baseBytes[i + j] != _valueBytes[i]) { | |
break; | |
} | |
} | |
if (i == _valueBytes.length) | |
return true; | |
} | |
return false; | |
} | |
function strConcat(string memory a, string memory b, string memory c) private pure returns (string memory) { | |
return string(abi.encodePacked(a,b,c)); | |
} | |
struct Node { | |
Request request; | |
Types.OptionU64 prev; | |
Types.OptionU64 next; | |
} | |
struct Request { | |
bytes30 serviceNumber; | |
uint64 date; | |
uint64 regNum; | |
uint128 dateRegNumPair; | |
Types.DeclarantType declarantType; | |
string declarantName; | |
uint64 primaryFairId; | |
uint64 currentFairId; | |
uint64[] fairIds; | |
Types.Speciality speciality; | |
StatusPeriod[] periods; | |
uint64 district; // округ | |
uint64 region; // район | |
Types.StatusUpdate[] statusUpdates; | |
string statusUpdatesNotes; | |
string details; | |
bytes16 userId; | |
} | |
struct StatusPeriod { | |
uint64 date; | |
uint64 status; | |
uint64 fairId; | |
} | |
} | |
// Снапшот сезона. Содержит также правила определения, должна ли распределяться заявка. Возможно необходимо разделение на два контракта. Впрочем, в случае совпадения по вторичному распределению это необязательно | |
contract SeasonSnapshot is Owned { | |
address public seasonAddress; | |
uint64 public currentPosition; // как и во всех контрактах распределения, формируем стейт-машину, и грузим заявки батчами | |
bool public isPositionSet; // true если был вызван loadHistoricalIndices | |
uint64[] historicalIndices; // список индексов всех заявок в порядке историчности | |
// сами заявки в порядке историчности. Тут только заявки, которые прошли фильтр и должны распределяться. | |
Types.DistributorRequest[] invididualsRequests; | |
Types.DistributorRequest[] farmerRequests; | |
Types.DistributorRequest[] ieLeRequests; | |
constructor(address seasonAddress_) public { | |
ISeason season = ISeason(seasonAddress_); | |
require(season.owner() == owner); | |
seasonAddress = seasonAddress_; | |
} | |
function loadHistoricalIndices() public onlyOwner { | |
require(!isPositionSet, "Position is already set"); | |
ISeason season = ISeason(seasonAddress); | |
uint64[] memory indices = season.getHistoricalIndices(); | |
require (indices.length > 0); | |
for (uint i = 0; i < indices.length; i++) { | |
uint64 index = indices[i]; | |
historicalIndices.push(index); | |
} | |
isPositionSet = true; | |
} | |
function isLoaded() public view returns (bool) { | |
return isPositionSet && currentPosition >= historicalIndices.length; | |
} | |
// Загрузка заявок. Заявки, которые не должны проходить распределение (в плохих статусах) отбрасываются. | |
function loadRequests() public onlyOwner { | |
require(isPositionSet, "Position is not set"); | |
require(!isLoaded(), "Is already loaded"); | |
ISeason season = ISeason(seasonAddress); | |
uint64 batchSize = 500; | |
uint64 diff = uint64(historicalIndices.length) - currentPosition; | |
uint64 iterCount = diff > batchSize ? batchSize : diff; | |
for (uint i = 0; i < iterCount; i++) { | |
uint64 historicalIndex = historicalIndices[currentPosition]; | |
currentPosition++; | |
Types.DistributorRequest memory request = getRequest(season, historicalIndex); | |
(uint64[] memory dates, uint64[] memory statusCodes) = getValidPeriods(request.periods, request.periodStatusCodes); | |
if (dates.length == 0) { | |
continue; | |
} | |
request.periods = dates; | |
request.periodStatusCodes = statusCodes; | |
if (request.declarantType == Types.DeclarantType.Individual) { | |
invididualsRequests.push(request); | |
} | |
else if (request.declarantType == Types.DeclarantType.Farmer && request.speciality == Types.Speciality.Vegetables) { | |
// only requests with vegetables speciality gets promoted | |
farmerRequests.push(request); | |
} | |
else if (request.declarantType == Types.DeclarantType.IndividualEntrepreneur | |
|| request.declarantType == Types.DeclarantType.LegalEntity | |
|| request.declarantType == Types.DeclarantType.Farmer | |
|| request.declarantType == Types.DeclarantType.IndividualAsIndividualEntrepreneur) { | |
ieLeRequests.push(request); | |
} else { | |
require (false, "Unexpected declarant type"); | |
} | |
} | |
} | |
// Преобразование заявки из сезона в заявку для распределения | |
function getRequest(ISeason season, uint64 index) private view returns (Types.DistributorRequest memory) { | |
(bytes30 serviceNumber, Types.DeclarantType declarantType, uint64[] memory fairIds, Types.Speciality speciality, uint64[] memory periods, bytes16 userId, uint64[] memory periodStatusCodes) = season.getDistributionRequestByIndex(index); | |
return Types.DistributorRequest(serviceNumber, userId, declarantType, fairIds, periods, periodStatusCodes, speciality); | |
} | |
function getIndividualRequestsCount() public view returns (uint64) { | |
return uint64(invididualsRequests.length); | |
} | |
function getIndividualRequest(uint index) public view returns (bytes30, bytes16, Types.DeclarantType, uint64[] memory, uint64[] memory, uint64[] memory, Types.Speciality speciality) { | |
Types.DistributorRequest storage request = invididualsRequests[index]; | |
return (request.serviceNumber, request.userId, request.declarantType, request.fairIds, request.periods, request.periodStatusCodes, request.speciality); | |
} | |
function getFarmerRequestsCount() public view returns (uint64) { | |
return uint64(farmerRequests.length); | |
} | |
function getFarmerRequest(uint index) public view returns (bytes30, bytes16, Types.DeclarantType, uint64[] memory, uint64[] memory, uint64[] memory, Types.Speciality speciality) { | |
Types.DistributorRequest storage request = farmerRequests[index]; | |
return (request.serviceNumber, request.userId, request.declarantType, request.fairIds, request.periods, request.periodStatusCodes, request.speciality); | |
} | |
function getLeRequestsCount() public view returns (uint64) { | |
return uint64(ieLeRequests.length); | |
} | |
function getLeRequest(uint index) public view returns (bytes30, bytes16, Types.DeclarantType, uint64[] memory, uint64[] memory, uint64[] memory, Types.Speciality speciality) { | |
Types.DistributorRequest storage request = ieLeRequests[index]; | |
return (request.serviceNumber, request.userId, request.declarantType, request.fairIds, request.periods, request.periodStatusCodes, request.speciality); | |
} | |
function getHistoricalIndicesCount() public view returns (uint64) { | |
return uint64(historicalIndices.length); | |
} | |
// функция определяет, на какие периоды заявка попадает | |
function getValidPeriods(uint64[] memory dates, uint64[] memory statusCodes) private pure returns (uint64[] memory, uint64[] memory) { | |
require (dates.length == statusCodes.length); | |
require (dates.length > 0); | |
uint[] memory indices = new uint[](dates.length); | |
uint indicesCount = 0; | |
for (uint i = 0; i < dates.length; i++) { | |
if (!isBadStatus(statusCodes[i])) { | |
indices[indicesCount++] = i; | |
} | |
} | |
uint64[] memory resultDates = new uint64[](indicesCount); | |
uint64[] memory resultStatusCodes = new uint64[](indicesCount); | |
for (uint i = 0; i < indicesCount; i++) { | |
uint index = indices[i]; | |
resultDates[i] = dates[index]; | |
resultStatusCodes[i] = statusCodes[index]; | |
} | |
return (resultDates, resultStatusCodes); | |
} | |
function isBadStatus(uint64 statusCode) private pure returns (bool) { | |
return statusCode == 1080 | |
|| statusCode == 1190 | |
|| statusCode == 1086 | |
|| statusCode == 77051 | |
|| statusCode == 103099; | |
} | |
} | |
contract IndividualsDistributor is IDistributor { | |
address public seasonSnapshotAddress; | |
uint64 currentLoadPosition; | |
uint64 currentDistributionPosition; | |
bool isPositionSet; | |
bool public isFinalized; // флаг того, что распределение закончено, и все оставшиеся нераспределенные заявки попадают в листы ожидания | |
uint64 public requestsCount; | |
Types.DistributorRequest[] requests; | |
Types.FairPeriod[] allPeriods; | |
mapping(uint256 => bytes30) registeredDeclarantRequests; | |
mapping(uint256 => mapping(uint256 => Types.MaybeUninit)) fairsToPeriodsToSpecialitiesToPeriodIndex; | |
mapping(bytes30 => mapping(uint64 => Types.MaybeUninit)) serviceNumberToPeriodToRequestIndex; | |
mapping(uint64 => mapping(bytes30 => bool)) isRequestFullyProcessedForPeriod; | |
event Warning(string text); | |
constructor(address seasonSnapshotAddress_) public { | |
SeasonSnapshot seasonSnapshot = SeasonSnapshot(seasonSnapshotAddress_); | |
require(seasonSnapshot.owner() == owner); | |
seasonSnapshotAddress = seasonSnapshotAddress_; | |
requestsCount = seasonSnapshot.getIndividualRequestsCount(); | |
} | |
function isLoaded() public view returns (bool) { | |
return isPositionSet && | |
currentLoadPosition >= requestsCount; | |
} | |
function isDistributed() public view returns (bool) { | |
return isLoaded() && | |
currentDistributionPosition >= requestsCount; | |
} | |
function init(uint64[] memory fairsIds, uint64[] memory periods, Types.Speciality[] memory specialities, uint64[] memory placesCount) public onlyOrigin { | |
allPeriods.length = fairsIds.length; | |
for (uint i = 0; i < fairsIds.length; i++) { | |
Types.FairPeriod storage fairPeriod = allPeriods[i]; | |
fairPeriod.fairId = fairsIds[i]; | |
fairPeriod.date = periods[i]; | |
fairPeriod.placesCount = placesCount[i]; | |
fairPeriod.speciality = specialities[i]; | |
if (specialities[i] == Types.Speciality.Vegetables) { | |
// only vegetables are allowed for individuals | |
fairsToPeriodsToSpecialitiesToPeriodIndex[fairPeriod.fairId][fairPeriod.date] = Types.MaybeUninit(true, i); | |
} | |
} | |
isPositionSet = true; | |
} | |
function loadRequests() public onlyOrigin { | |
require(!isLoaded()); | |
SeasonSnapshot seasonSnapshot = SeasonSnapshot(seasonSnapshotAddress); | |
uint64 batchSize = 500; | |
uint64 diff = requestsCount - currentLoadPosition; | |
uint64 iterCount = diff > batchSize ? batchSize : diff; | |
for (uint i = 0; i < iterCount; i++) { | |
Types.DistributorRequest memory request = getRequest(seasonSnapshot, currentLoadPosition); | |
requests.push(request); | |
for (uint j = 0; j < request.periods.length; j++) { | |
uint64 periodId = request.periods[j]; | |
uint64 fairId = request.fairIds[j]; | |
Types.MaybeUninit storage periodIndex = fairsToPeriodsToSpecialitiesToPeriodIndex[fairId][periodId]; | |
if (!periodIndex.isInited) { | |
emit Warning(string(abi.encodePacked("Period not found in distribution command while loading: request ", request.serviceNumber, ", fair ", uint2str(fairId), ", period ", uint2str(periodId)))); | |
continue; // we didn't pass this period in distribution command - skipping unwanted request | |
} | |
Types.FairPeriod storage period = allPeriods[periodIndex.value]; | |
period.allRequests.push(request.serviceNumber); | |
serviceNumberToPeriodToRequestIndex[request.serviceNumber][periodId] = Types.MaybeUninit(true, currentLoadPosition); | |
} | |
currentLoadPosition++; | |
} | |
} | |
function tryApprove(uint64 fairId, uint64 date, Types.Speciality speciality, bytes30[] memory serviceNumbers) public onlyOrigin { | |
if (speciality != Types.Speciality.Vegetables) { | |
return; | |
} | |
Types.MaybeUninit storage periodIndex = fairsToPeriodsToSpecialitiesToPeriodIndex[fairId][date]; | |
if (!periodIndex.isInited) { | |
emit Warning(string(abi.encodePacked("Period not found in distribution command while loading: request ", serviceNumber, ", fair ", uint2str(fairId), ", period ", uint2str(date)))); | |
return; // we didn't pass this period in distribution command - skipping unwanted request | |
} | |
Types.FairPeriod storage period = allPeriods[periodIndex.value]; | |
require(period.placesCount >= serviceNumbers.length); | |
for (uint i = 0; i < serviceNumbers.length; i++) { | |
bytes30 serviceNumber = serviceNumbers[i]; | |
uint64 number = uint64(i); | |
Types.MaybeUninit storage requestIndex = serviceNumberToPeriodToRequestIndex[serviceNumber][date]; | |
if (!requestIndex.isInited) { | |
continue; | |
} | |
Types.DistributorRequest storage request = requests[requestIndex.value]; | |
uint256 userPeriodId = uint256(uint128(request.userId)) << 128 | uint256(date); // it's basically a tuple (period, userId) | |
approveRequest(period, serviceNumber, date, userPeriodId, Types.OptionU64(true, number)); | |
} | |
} | |
// сама функция распределения. Этапы отмечены согласно ТЗ, например 1.1 означает что тут выполняется этап 1.1 алгоритма распределения из ТЗ | |
function distribute() public onlyOrigin { | |
require(!isDistributed()); | |
uint64 batchSize = 500; | |
uint64 diff = requestsCount - currentDistributionPosition; | |
uint64 iterCount = diff > batchSize ? batchSize : diff; // последняя итерация будет иметь размер меньше батча | |
for (uint i = 0; i < iterCount; i++) { | |
Types.DistributorRequest storage request = requests[currentDistributionPosition]; // 1.1 | |
for (uint j = 0; j < request.periods.length; j++) { | |
uint64 periodId = request.periods[j]; | |
uint64 fairId = request.fairIds[j]; | |
uint256 userPeriodId = uint256(uint128(request.userId)) << 128 | uint256(periodId); // it's basically a tuple (period, userId) | |
Types.MaybeUninit storage periodIndex = fairsToPeriodsToSpecialitiesToPeriodIndex[fairId][periodId]; | |
if (!periodIndex.isInited) { | |
emit Warning(string(abi.encodePacked("Period not found in distribution command while distributing: request ", request.serviceNumber, ", fair ", uint2str(fairId), ", period ", uint2str(periodId)))); | |
continue; // we didn't pass this period in distribution command - skipping unwanted request | |
} | |
Types.FairPeriod storage period = allPeriods[periodIndex.value]; | |
if (isRequestFullyProcessedForPeriod[periodId][request.serviceNumber]) { | |
continue; | |
} | |
if (registeredDeclarantRequests[userPeriodId] != 0 && registeredDeclarantRequests[userPeriodId] != request.serviceNumber) { | |
period.rejectedServiceNumbers.push(request.serviceNumber); | |
isRequestFullyProcessedForPeriod[periodId][request.serviceNumber] = true; | |
continue; // skip registered Individuals | |
} | |
if (period.placesCount > 0) { | |
approveRequest(period, request.serviceNumber, periodId, userPeriodId, Types.OptionU64(false, 0)); | |
} | |
} | |
currentDistributionPosition++; | |
} | |
} | |
function approveRequest(Types.FairPeriod storage period, bytes30 serviceNumber, uint64 periodId, uint256 userPeriodId, Types.OptionU64 memory number) private { | |
period.placesCount--; | |
period.approvedPlaces.push(Types.ApprovedPlace(serviceNumber, number)); // 1.2 | |
registeredDeclarantRequests[userPeriodId] = serviceNumber; // 1.3 setting mark that other requests for this declarant should be declined | |
isRequestFullyProcessedForPeriod[periodId][serviceNumber] = true; // 1.3 setting mark that request was processed | |
string memory place; | |
if (number.hasValue) { | |
place = uint2str(number.value); | |
} else { | |
place = "-"; | |
} | |
} | |
// Возвращает оставшиеся свободные места. Необходимо для перераспределения | |
function getPlaces() public view returns(uint64[] memory, uint64[] memory, bool[] memory) { | |
uint64[] memory total = new uint64[](getPeriodsCount()); | |
uint64[] memory unoccupied = new uint64[](total.length); | |
bool[] memory areUpdatable = new bool[](total.length); | |
for (uint i = 0; i < total.length; i++) { | |
if (allPeriods[i].speciality == Types.Speciality.Vegetables) { | |
total[i] = allPeriods[i].placesCount + uint64(allPeriods[i].approvedPlaces.length); | |
unoccupied[i] = allPeriods[i].placesCount; | |
areUpdatable[i] = true; | |
} | |
} | |
return (total, unoccupied, areUpdatable); | |
} | |
// Возвращает количество потенциально возможнх заявок на места. Используется для первичного перераспределения | |
function getRequestedPlaces() public view returns(uint64[] memory) { | |
uint64[] memory result = new uint64[](getPeriodsCount()); | |
for (uint i = 0; i < result.length; i++) { | |
if (allPeriods[i].speciality == Types.Speciality.Vegetables) { | |
result[i] = uint64(allPeriods[i].allRequests.length); | |
} | |
} | |
return result; | |
} | |
function getPeriodsCount() public view returns(uint64) { | |
return uint64(allPeriods.length); | |
} | |
function getPeriod(uint64 index) public view returns(uint64, uint64, bytes30[] memory, bytes30[] memory, bytes30[] memory, Types.Speciality) { | |
Types.FairPeriod storage period = allPeriods[index]; | |
return (period.fairId, period.date, period.resultingServiceNumbers, period.waitingList, period.rejectedServiceNumbers, period.speciality); | |
} | |
function updatePlaces(uint64[] memory placesCounts) public onlyOrigin { | |
for (uint i = 0; i < placesCounts.length; i++) { | |
if (allPeriods[i].speciality == Types.Speciality.Vegetables) { | |
allPeriods[i].placesCount = placesCounts[i]; | |
} | |
} | |
currentDistributionPosition = 0; | |
} | |
function finalize(Types.DistributionType distributionType) public onlyOrigin { | |
require(!isFinalized); | |
for (uint i = 0; i < allPeriods.length; i++) { | |
Types.FairPeriod storage period = allPeriods[i]; | |
for (uint j = 0; j < period.allRequests.length; j++) { | |
bytes30 serviceNumber = period.allRequests[j]; | |
if (!isRequestFullyProcessedForPeriod[period.date][serviceNumber]) { | |
if (distributionType == Types.DistributionType.Initial) { | |
period.waitingList.push(serviceNumber); | |
} else { | |
period.rejectedServiceNumbers.push(serviceNumber); | |
} | |
} | |
} | |
period.resultingServiceNumbers = new bytes30[](period.approvedPlaces.length + period.placesCount); | |
for (uint k = 0; k < period.approvedPlaces.length; k++) { | |
Types.ApprovedPlace memory approvedPlace = period.approvedPlaces[k]; | |
if (approvedPlace.number.hasValue) { | |
if (approvedPlace.number.value < period.resultingServiceNumbers.length) { | |
period.resultingServiceNumbers[approvedPlace.number.value] = approvedPlace.serviceNumber; | |
} | |
else { | |
emit Warning(string(abi.encodePacked("Invalid place: request ", approvedPlace.serviceNumber, ", place ", uint2str(approvedPlace.number.value), ", length ", uint2str(period.resultingServiceNumbers.length), ", period ", uint2str(period.date)))); | |
} | |
} else { | |
for (uint insertionIndex = 0; insertionIndex < period.resultingServiceNumbers.length; insertionIndex++) { | |
if (period.resultingServiceNumbers[insertionIndex] == 0) { | |
period.resultingServiceNumbers[insertionIndex] = approvedPlace.serviceNumber; | |
break; | |
} | |
} | |
} | |
} | |
} | |
isFinalized = true; | |
} | |
function getRequest(SeasonSnapshot seasonSnapshot, uint64 index) private view returns (Types.DistributorRequest memory) { | |
(bytes30 serviceNumber, bytes16 userId, Types.DeclarantType declarantType, uint64[] memory fairIds, uint64[] memory periods, uint64[] memory statusCodes, Types.Speciality speciality) = seasonSnapshot.getIndividualRequest(index); | |
return Types.DistributorRequest(serviceNumber, userId, declarantType, fairIds, periods, statusCodes, speciality); | |
} | |
function uint2str(uint _i) internal pure returns (string memory _uintAsString) { | |
if (_i == 0) { | |
return "0"; | |
} | |
uint j = _i; | |
uint len; | |
while (j != 0) { | |
len++; | |
j /= 10; | |
} | |
bytes memory bstr = new bytes(len); | |
uint k = len - 1; | |
while (_i != 0) { | |
bstr[k--] = byte(uint8(48 + _i % 10)); | |
_i /= 10; | |
} | |
return string(bstr); | |
} | |
} | |
// Аналогично с распределением физлиц | |
contract FarmersDistributor is IDistributor { | |
address public seasonSnapshotAddress; | |
address public userPeriodsStorageAddress; | |
uint64 currentLoadPosition; | |
uint64 currentDistributionPosition; | |
bool isPositionSet; | |
bool shouldDeclineRequestsWithLoweredPriority; | |
bool public isFinalized; | |
uint64 public requestsCount; | |
Types.DistributorRequest[] requests; | |
Types.FairPeriod[] allPeriods; | |
mapping(uint64 => mapping(uint256 => bytes30)) fairsToRegisteredDeclarantRequests; | |
mapping(uint256 => mapping(uint256 => Types.MaybeUninit)) fairsToPeriodsToSpecialitiesToPeriodIndex; | |
mapping(bytes30 => mapping(uint64 => Types.MaybeUninit)) serviceNumberToPeriodToRequestIndex; | |
mapping(uint64 => mapping(bytes30 => bool)) isRequestFullyProcessedForPeriod; | |
event Warning(string text); | |
constructor(address seasonSnapshotAddress_, address userPeriodsStorageAddress_) public { // храним снапшот и список юзеров для которых удовлетворили заявки. Для всех кроме ФЛ | |
SeasonSnapshot seasonSnapshot = SeasonSnapshot(seasonSnapshotAddress_); | |
require(seasonSnapshot.owner() == owner); | |
userPeriodsStorageAddress = userPeriodsStorageAddress_; | |
seasonSnapshotAddress = seasonSnapshotAddress_; | |
requestsCount = seasonSnapshot.getFarmerRequestsCount(); | |
shouldDeclineRequestsWithLoweredPriority = true; | |
} | |
function isLoaded() public view returns (bool) { | |
return isPositionSet && | |
currentLoadPosition >= requestsCount; | |
} | |
function isDistributed() public view returns (bool) { | |
return isLoaded() && | |
currentDistributionPosition >= requestsCount; | |
} | |
function init(uint64[] memory fairsIds, uint64[] memory periods, Types.Speciality[] memory specialities, uint64[] memory placesCount) public onlyOrigin { | |
allPeriods.length = fairsIds.length; | |
for (uint i = 0; i < fairsIds.length; i++) { | |
Types.FairPeriod storage fairPeriod = allPeriods[i]; | |
fairPeriod.fairId = fairsIds[i]; | |
fairPeriod.date = periods[i]; | |
fairPeriod.placesCount = placesCount[i]; | |
fairPeriod.speciality = specialities[i]; | |
if (specialities[i] == Types.Speciality.Vegetables) { | |
// only vegetables are allowed for farmers | |
fairsToPeriodsToSpecialitiesToPeriodIndex[fairPeriod.fairId][fairPeriod.date] = Types.MaybeUninit(true, i); | |
} | |
} | |
isPositionSet = true; | |
} | |
function loadRequests() public onlyOrigin { | |
require(!isLoaded()); | |
SeasonSnapshot seasonSnapshot = SeasonSnapshot(seasonSnapshotAddress); | |
uint64 batchSize = 500; | |
uint64 diff = requestsCount - currentLoadPosition; | |
uint64 iterCount = diff > batchSize ? batchSize : diff; | |
for (uint i = 0; i < iterCount; i++) { | |
Types.DistributorRequest memory request = getRequest(seasonSnapshot, currentLoadPosition); | |
requests.push(request); | |
for (uint j = 0; j < request.periods.length; j++) { | |
uint64 periodId = request.periods[j]; | |
uint64 fairId = request.fairIds[j]; | |
Types.MaybeUninit storage periodIndex = fairsToPeriodsToSpecialitiesToPeriodIndex[fairId][periodId]; | |
if (!periodIndex.isInited) { | |
emit Warning(string(abi.encodePacked("Period not found in distribution command while loading: request ", request.serviceNumber, ", fair ", uint2str(fairId), ", period ", uint2str(periodId)))); | |
continue; // we didn't pass this period in distribution command - skipping unwanted request | |
} | |
Types.FairPeriod storage period = allPeriods[periodIndex.value]; | |
period.allRequests.push(request.serviceNumber); | |
serviceNumberToPeriodToRequestIndex[request.serviceNumber][periodId] = Types.MaybeUninit(true, currentLoadPosition); | |
} | |
currentLoadPosition++; | |
} | |
} | |
function tryApprove(uint64 fairId, uint64 date, Types.Speciality speciality, bytes30[] memory serviceNumbers) public onlyOrigin { | |
if (speciality != Types.Speciality.Vegetables) { | |
return; | |
} | |
Types.MaybeUninit storage periodIndex = fairsToPeriodsToSpecialitiesToPeriodIndex[fairId][date]; | |
if (!periodIndex.isInited) { | |
emit Warning(string(abi.encodePacked("Period not found in distribution command while loading: request ", serviceNumber, ", fair ", uint2str(fairId), ", period ", uint2str(date)))); | |
return; // we didn't pass this period in distribution command - skipping unwanted request | |
} | |
Types.FairPeriod storage period = allPeriods[periodIndex.value]; | |
require(period.placesCount >= serviceNumbers.length); | |
UserPeriodsStorage userPeriodsStorage = UserPeriodsStorage(userPeriodsStorageAddress); | |
for (uint i = 0; i < serviceNumbers.length; i++) { | |
bytes30 serviceNumber = serviceNumbers[i]; | |
uint64 number = uint64(i); | |
Types.MaybeUninit storage requestIndex = serviceNumberToPeriodToRequestIndex[serviceNumber][date]; | |
if (!requestIndex.isInited) { | |
continue; | |
} | |
Types.DistributorRequest storage request = requests[requestIndex.value]; | |
uint256 userPeriodId = uint256(uint128(request.userId)) << 128 | uint256(date); // it's basically a tuple (period, userId) | |
approveRequest(period, serviceNumber, date, userPeriodId, userPeriodsStorage, Types.OptionU64(true, number)); | |
} | |
} | |
function distribute() public onlyOrigin { | |
require(!isDistributed()); | |
uint64 batchSize = 500; | |
uint64 diff = requestsCount - currentDistributionPosition; | |
uint64 iterCount = diff > batchSize ? batchSize : diff; | |
for (uint i = 0; i < iterCount; i++) { | |
processRequest(currentDistributionPosition); | |
currentDistributionPosition++; | |
} | |
// При первом проходе алгоритма мы отклоняем все заявки с пониженным приоритетом. Они будут рассмотрены на второй итерации после того, Как все заявки с повышенным приоритетом будут обработаны | |
if (isDistributed() && shouldDeclineRequestsWithLoweredPriority) { | |
shouldDeclineRequestsWithLoweredPriority = false; // 2.5 | |
currentDistributionPosition = 0; | |
} | |
} | |
function processRequest(uint position) private { | |
UserPeriodsStorage userPeriodsStorage = UserPeriodsStorage(userPeriodsStorageAddress); | |
Types.DistributorRequest storage request = requests[position]; // 2.1 | |
for (uint j = 0; j < request.periods.length; j++) { | |
uint64 periodId = request.periods[j]; | |
uint64 fairId = request.fairIds[j]; | |
uint256 userPeriodId = uint256(uint128(request.userId)) << 128 | uint256(periodId); // it's basically a tuple (period, userId) | |
Types.MaybeUninit storage periodIndex = fairsToPeriodsToSpecialitiesToPeriodIndex[fairId][periodId]; | |
if (!periodIndex.isInited) { | |
emit Warning(string(abi.encodePacked("Period not found in distribution command while distributing: request ", request.serviceNumber, ", fair ", uint2str(fairId), ", period ", uint2str(periodId)))); | |
continue; // we didn't pass this period in distribution command - skipping unwanted request | |
} | |
Types.FairPeriod storage period = allPeriods[periodIndex.value]; | |
if (isRequestFullyProcessedForPeriod[periodId][request.serviceNumber]) { | |
continue; // skipping already processed requests | |
} | |
if (shouldDeclineRequestsWithLoweredPriority && userPeriodsStorage.declarantRegisteredRequestForPeriod(request.userId, periodId) != 0) { | |
continue; // skip request with lowered priority | |
} | |
bytes30 processedRequest = fairsToRegisteredDeclarantRequests[fairId][userPeriodId]; | |
if (processedRequest != 0 && processedRequest != request.serviceNumber) { | |
period.rejectedServiceNumbers.push(request.serviceNumber); | |
isRequestFullyProcessedForPeriod[periodId][request.serviceNumber] = true; | |
continue; // skip registered periods | |
} | |
if (period.placesCount > 0) { | |
approveRequest(period, request.serviceNumber, periodId, userPeriodId, userPeriodsStorage, Types.OptionU64(false, 0)); | |
} | |
} | |
} | |
function approveRequest(Types.FairPeriod storage period, bytes30 serviceNumber, uint64 periodId, uint256 userPeriodId, UserPeriodsStorage userPeriodsStorage, Types.OptionU64 memory number) private { | |
period.placesCount--; | |
period.approvedPlaces.push(Types.ApprovedPlace(serviceNumber, number)); // 2.2 | |
userPeriodsStorage.addDeclarant(userPeriodId, serviceNumber); // 2.3 setting mark that other requests for this declarant should be declined | |
fairsToRegisteredDeclarantRequests[period.fairId][userPeriodId] = serviceNumber; // 2.3 setting mark that other requests for this declarant should be declined | |
isRequestFullyProcessedForPeriod[periodId][serviceNumber] = true; // 2.3 setting mark that request was processed | |
string memory place; | |
if (number.hasValue) { | |
place = uint2str(number.value); | |
} else { | |
place = "-"; | |
} | |
} | |
function getPlaces() public view returns(uint64[] memory, uint64[] memory, bool[] memory) { | |
uint64[] memory total = new uint64[](getPeriodsCount()); | |
uint64[] memory unoccupied = new uint64[](total.length); | |
bool[] memory areUpdatable = new bool[](total.length); | |
for (uint i = 0; i < total.length; i++) { | |
if (allPeriods[i].speciality == Types.Speciality.Vegetables) { | |
total[i] = allPeriods[i].placesCount + uint64(allPeriods[i].approvedPlaces.length); | |
unoccupied[i] = allPeriods[i].placesCount; | |
areUpdatable[i] = true; | |
} | |
} | |
return (total, unoccupied, areUpdatable); | |
} | |
function getRequestedPlaces() public view returns(uint64[] memory) { | |
uint64[] memory result = new uint64[](getPeriodsCount()); | |
for (uint i = 0; i < result.length; i++) { | |
if (allPeriods[i].speciality == Types.Speciality.Vegetables) { | |
result[i] = uint64(allPeriods[i].allRequests.length); | |
} | |
} | |
return result; | |
} | |
function getPeriodsCount() public view returns(uint64) { | |
return uint64(allPeriods.length); | |
} | |
function getPeriod(uint64 index) public view returns(uint64, uint64, bytes30[] memory, bytes30[] memory, bytes30[] memory, Types.Speciality) { | |
Types.FairPeriod storage period = allPeriods[index]; | |
return (period.fairId, period.date, period.resultingServiceNumbers, period.waitingList, period.rejectedServiceNumbers, period.speciality); | |
} | |
function updatePlaces(uint64[] memory placesCounts) public onlyOrigin { | |
for (uint i = 0; i < placesCounts.length; i++) { | |
if (allPeriods[i].speciality == Types.Speciality.Vegetables) { | |
allPeriods[i].placesCount = placesCounts[i]; | |
} | |
} | |
currentDistributionPosition = 0; | |
shouldDeclineRequestsWithLoweredPriority = true; | |
} | |
function finalize(Types.DistributionType distributionType) public onlyOrigin { | |
require(!isFinalized); | |
for (uint i = 0; i < allPeriods.length; i++) { | |
Types.FairPeriod storage period = allPeriods[i]; | |
for (uint j = 0; j < period.allRequests.length; j++) { | |
bytes30 serviceNumber = period.allRequests[j]; | |
if (!isRequestFullyProcessedForPeriod[period.date][serviceNumber]) { | |
if (distributionType == Types.DistributionType.Initial) { | |
period.waitingList.push(serviceNumber); | |
} else { | |
period.rejectedServiceNumbers.push(serviceNumber); | |
} | |
} | |
} | |
period.resultingServiceNumbers = new bytes30[](period.approvedPlaces.length + period.placesCount); | |
for (uint k = 0; k < period.approvedPlaces.length; k++) { | |
Types.ApprovedPlace memory approvedPlace = period.approvedPlaces[k]; | |
if (approvedPlace.number.hasValue) { | |
if (approvedPlace.number.value < period.resultingServiceNumbers.length) { | |
period.resultingServiceNumbers[approvedPlace.number.value] = approvedPlace.serviceNumber; | |
} | |
else { | |
emit Warning(string(abi.encodePacked("Invalid place: request ", approvedPlace.serviceNumber, ", place ", uint2str(approvedPlace.number.value), ", length ", uint2str(period.resultingServiceNumbers.length), ", period ", uint2str(period.date)))); | |
} | |
} else { | |
for (uint insertionIndex = 0; insertionIndex < period.resultingServiceNumbers.length; insertionIndex++) { | |
if (period.resultingServiceNumbers[insertionIndex] == 0) { | |
period.resultingServiceNumbers[insertionIndex] = approvedPlace.serviceNumber; | |
break; | |
} | |
} | |
} | |
} | |
} | |
isFinalized = true; | |
} | |
function getRequest(SeasonSnapshot seasonSnapshot, uint64 index) private view returns (Types.DistributorRequest memory) { | |
(bytes30 serviceNumber, bytes16 userId, Types.DeclarantType declarantType, uint64[] memory fairIds, uint64[] memory periods, uint64[] memory statusCodes, Types.Speciality speciality) = seasonSnapshot.getFarmerRequest(index); | |
return Types.DistributorRequest(serviceNumber, userId, declarantType, fairIds, periods, statusCodes, speciality); | |
} | |
function uint2str(uint _i) internal pure returns (string memory _uintAsString) { | |
if (_i == 0) { | |
return "0"; | |
} | |
uint j = _i; | |
uint len; | |
while (j != 0) { | |
len++; | |
j /= 10; | |
} | |
bytes memory bstr = new bytes(len); | |
uint k = len - 1; | |
while (_i != 0) { | |
bstr[k--] = byte(uint8(48 + _i % 10)); | |
_i /= 10; | |
} | |
return string(bstr); | |
} | |
} | |
// Аналогично распределению КФХ | |
contract LeDistributor is IDistributor { | |
address public seasonSnapshotAddress; | |
address public userPeriodsStorageAddress; | |
uint64 currentLoadPosition; | |
uint64 currentDistributionPosition; | |
bool isPositionSet; | |
bool shouldDeclineRequestsWithLoweredPriority; | |
bool public isFinalized; | |
uint64 public requestsCount; | |
Types.DistributorRequest[] requests; | |
Types.FairPeriod[] allPeriods; | |
mapping(uint64 => mapping(uint256 => bytes30)) fairsToRegisteredDeclarantRequests; | |
mapping(uint256 => mapping(uint256 => mapping(uint256 => Types.MaybeUninit))) fairsToPeriodsToSpecialitiesToPeriodIndex; | |
mapping(bytes30 => mapping(uint64 => Types.MaybeUninit)) serviceNumberToPeriodToRequestIndex; | |
mapping(uint64 => mapping(bytes30 => bool)) isRequestFullyProcessedForPeriod; | |
event Warning(string text); | |
constructor(address seasonSnapshotAddress_, address userPeriodsStorageAddress_) public { | |
SeasonSnapshot seasonSnapshot = SeasonSnapshot(seasonSnapshotAddress_); | |
require(seasonSnapshot.owner() == owner); | |
seasonSnapshotAddress = seasonSnapshotAddress_; | |
userPeriodsStorageAddress = userPeriodsStorageAddress_; | |
requestsCount = seasonSnapshot.getLeRequestsCount(); | |
shouldDeclineRequestsWithLoweredPriority = true; | |
} | |
function isLoaded() public view returns (bool) { | |
return isPositionSet && | |
currentLoadPosition >= requestsCount; | |
} | |
function isDistributed() public view returns (bool) { | |
return isLoaded() && | |
currentDistributionPosition >= requestsCount; | |
} | |
function init(uint64[] memory fairsIds, uint64[] memory periods, Types.Speciality[] memory specialities, uint64[] memory placesCount) public onlyOrigin { | |
allPeriods.length = fairsIds.length; | |
for (uint i = 0; i < fairsIds.length; i++) { | |
Types.FairPeriod storage fairPeriod = allPeriods[i]; | |
fairPeriod.fairId = fairsIds[i]; | |
fairPeriod.date = periods[i]; | |
fairPeriod.placesCount = placesCount[i]; | |
fairPeriod.speciality = specialities[i]; | |
fairsToPeriodsToSpecialitiesToPeriodIndex[fairsIds[i]][periods[i]][uint256(specialities[i])] = Types.MaybeUninit(true, i); | |
} | |
isPositionSet = true; | |
} | |
function loadRequests() public onlyOrigin { | |
require(!isLoaded()); | |
SeasonSnapshot seasonSnapshot = SeasonSnapshot(seasonSnapshotAddress); | |
uint64 batchSize = 500; | |
uint64 diff = requestsCount - currentLoadPosition; | |
uint64 iterCount = diff > batchSize ? batchSize : diff; | |
for (uint i = 0; i < iterCount; i++) { | |
Types.DistributorRequest memory request = getRequest(seasonSnapshot, currentLoadPosition); | |
requests.push(request); | |
Types.Speciality speciality = request.speciality; | |
for (uint j = 0; j < request.periods.length; j++) { | |
uint64 periodId = request.periods[j]; | |
uint64 fairId = request.fairIds[j]; | |
Types.MaybeUninit storage periodIndex = fairsToPeriodsToSpecialitiesToPeriodIndex[fairId][periodId][uint256(speciality)]; | |
if (!periodIndex.isInited) { | |
emit Warning(string(abi.encodePacked("Period not found in distribution command while loading: request ", request.serviceNumber, ", fair ", uint2str(fairId), ", period ", uint2str(periodId)))); | |
continue; // we didn't pass this period in distribution command - skipping unwanted request | |
} | |
Types.FairPeriod storage period = allPeriods[periodIndex.value]; | |
period.allRequests.push(request.serviceNumber); | |
serviceNumberToPeriodToRequestIndex[request.serviceNumber][periodId] = Types.MaybeUninit(true, currentLoadPosition); | |
} | |
currentLoadPosition++; | |
} | |
} | |
function tryApprove(uint64 fairId, uint64 date, Types.Speciality speciality, bytes30[] memory serviceNumbers) public onlyOrigin { | |
UserPeriodsStorage userPeriodsStorage = UserPeriodsStorage(userPeriodsStorageAddress); | |
Types.MaybeUninit storage periodIndex = fairsToPeriodsToSpecialitiesToPeriodIndex[fairId][date][uint256(speciality)]; | |
if (!periodIndex.isInited) { | |
emit Warning(string(abi.encodePacked("Period not found in distribution command while loading: request ", serviceNumber, ", fair ", uint2str(fairId), ", period ", uint2str(date)))); | |
return; // we didn't pass this period in distribution command - skipping unwanted request | |
} | |
Types.FairPeriod storage period = allPeriods[periodIndex.value]; | |
require(period.placesCount >= serviceNumbers.length); | |
for (uint i = 0; i < serviceNumbers.length; i++) { | |
bytes30 serviceNumber = serviceNumbers[i]; | |
uint64 number = uint64(i); | |
Types.MaybeUninit storage requestIndex = serviceNumberToPeriodToRequestIndex[serviceNumber][date]; | |
if (!requestIndex.isInited) { | |
continue; | |
} | |
Types.DistributorRequest storage request = requests[requestIndex.value]; | |
uint256 userPeriodId = uint256(uint128(request.userId)) << 128 | uint256(date); // it's basically a tuple (period, userId) | |
approveRequest(period, serviceNumber, date, userPeriodId, userPeriodsStorage, Types.OptionU64(true, number)); | |
} | |
} | |
function distribute() public onlyOrigin { | |
require(!isDistributed()); | |
uint64 batchSize = 500; | |
uint64 diff = requestsCount - currentDistributionPosition; | |
uint64 iterCount = diff > batchSize ? batchSize : diff; | |
UserPeriodsStorage userPeriodsStorage = UserPeriodsStorage(userPeriodsStorageAddress); | |
for (uint i = 0; i < iterCount; i++) { | |
Types.DistributorRequest storage request = requests[currentDistributionPosition]; // 1.1 | |
Types.Speciality speciality = request.speciality; | |
for (uint j = 0; j < request.periods.length; j++) { | |
uint64 periodId = request.periods[j]; | |
uint64 fairId = request.fairIds[j]; | |
uint256 userPeriodId = uint256(uint128(request.userId)) << 128 | uint256(periodId); // it's basically a tuple (period, userId) | |
Types.MaybeUninit storage periodIndex = fairsToPeriodsToSpecialitiesToPeriodIndex[fairId][periodId][uint256(speciality)]; | |
if (!periodIndex.isInited) { | |
emit Warning(string(abi.encodePacked("Period not found in distribution command while distributing: request ", request.serviceNumber, ", fair ", uint2str(fairId), ", period ", uint2str(periodId)))); | |
continue; // we didn't pass this period in distribution command - skipping unwanted request | |
} | |
Types.FairPeriod storage period = allPeriods[periodIndex.value]; | |
if (isRequestFullyProcessedForPeriod[periodId][request.serviceNumber]) { | |
continue; // skipping already processed requsets | |
} | |
if (shouldDeclineRequestsWithLoweredPriority && userPeriodsStorage.declarantRegisteredRequestForPeriod(request.userId, periodId) != 0) { | |
continue; // skip request with lowered priority | |
} | |
if (fairsToRegisteredDeclarantRequests[fairId][userPeriodId] != 0 && fairsToRegisteredDeclarantRequests[fairId][userPeriodId] != request.serviceNumber) { | |
period.rejectedServiceNumbers.push(request.serviceNumber); | |
isRequestFullyProcessedForPeriod[periodId][request.serviceNumber] = true; | |
continue; // skip registered periods | |
} | |
if (period.placesCount > 0) { | |
approveRequest(period, request.serviceNumber, periodId, userPeriodId, userPeriodsStorage, Types.OptionU64(false, 0)); | |
} | |
} | |
currentDistributionPosition++; | |
} | |
if (isDistributed() && shouldDeclineRequestsWithLoweredPriority) { | |
shouldDeclineRequestsWithLoweredPriority = false; // 3.5 | |
currentDistributionPosition = 0; | |
} | |
} | |
function approveRequest(Types.FairPeriod storage period, bytes30 serviceNumber, uint64 periodId, uint256 userPeriodId, UserPeriodsStorage userPeriodsStorage, Types.OptionU64 memory number) private { | |
period.placesCount--; | |
period.approvedPlaces.push(Types.ApprovedPlace(serviceNumber, number)); // 3.2 | |
userPeriodsStorage.addDeclarant(userPeriodId, serviceNumber); // 3.3 setting mark that other requests for this declarant should be declined | |
fairsToRegisteredDeclarantRequests[period.fairId][userPeriodId] = serviceNumber; // 3.3 setting mark that other requests for this declarant should be declined | |
isRequestFullyProcessedForPeriod[periodId][serviceNumber] = true; // 3.3 setting mark that request was processed | |
string memory place; | |
if (number.hasValue) { | |
place = uint2str(number.value); | |
} else { | |
place = "-"; | |
} | |
} | |
function getPlaces() public view returns(uint64[] memory, uint64[] memory, bool[] memory) { | |
uint64[] memory total = new uint64[](getPeriodsCount()); | |
uint64[] memory unoccupied = new uint64[](total.length); | |
bool[] memory areUpdatable = new bool[](total.length); | |
for (uint i = 0; i < total.length; i++) { | |
if (allPeriods[i].speciality == Types.Speciality.Vegetables) { | |
total[i] = allPeriods[i].placesCount + uint64(allPeriods[i].approvedPlaces.length); | |
unoccupied[i] = allPeriods[i].placesCount; | |
areUpdatable[i] = true; | |
} | |
} | |
return (total, unoccupied, areUpdatable); | |
} | |
function getRequestedPlaces() public view returns(uint64[] memory) { | |
uint64[] memory result = new uint64[](getPeriodsCount()); | |
for (uint i = 0; i < result.length; i++) { | |
if (allPeriods[i].speciality == Types.Speciality.Vegetables) { | |
result[i] = uint64(allPeriods[i].allRequests.length); | |
} | |
} | |
return result; | |
} | |
function getPeriodsCount() public view returns(uint64) { | |
return uint64(allPeriods.length); | |
} | |
function getPeriod(uint64 index) public view returns(uint64, uint64, bytes30[] memory, bytes30[] memory, bytes30[] memory, Types.Speciality) { | |
Types.FairPeriod storage period = allPeriods[index]; | |
return (period.fairId, period.date, period.resultingServiceNumbers, period.waitingList, period.rejectedServiceNumbers, period.speciality); | |
} | |
function updatePlaces(uint64[] memory placesCounts) public onlyOrigin { | |
for (uint i = 0; i < placesCounts.length; i++) { | |
if (allPeriods[i].speciality == Types.Speciality.Vegetables) { | |
allPeriods[i].placesCount = placesCounts[i]; | |
} | |
} | |
currentDistributionPosition = 0; | |
shouldDeclineRequestsWithLoweredPriority = true; | |
} | |
function finalize(Types.DistributionType distributionType) public onlyOrigin { | |
require(!isFinalized); | |
for (uint i = 0; i < allPeriods.length; i++) { | |
Types.FairPeriod storage period = allPeriods[i]; | |
for (uint j = 0; j < period.allRequests.length; j++) { | |
bytes30 serviceNumber = period.allRequests[j]; | |
if (!isRequestFullyProcessedForPeriod[period.date][serviceNumber]) { | |
if (distributionType == Types.DistributionType.Initial) { | |
period.waitingList.push(serviceNumber); | |
} else { | |
period.rejectedServiceNumbers.push(serviceNumber); | |
} | |
} | |
} | |
period.resultingServiceNumbers = new bytes30[](period.approvedPlaces.length + period.placesCount); | |
for (uint k = 0; k < period.approvedPlaces.length; k++) { | |
Types.ApprovedPlace memory approvedPlace = period.approvedPlaces[k]; | |
if (approvedPlace.number.hasValue) { | |
if (approvedPlace.number.value < period.resultingServiceNumbers.length) { | |
period.resultingServiceNumbers[approvedPlace.number.value] = approvedPlace.serviceNumber; | |
} | |
else { | |
emit Warning(string(abi.encodePacked("Invalid place: request ", approvedPlace.serviceNumber, ", place ", uint2str(approvedPlace.number.value), ", length ", uint2str(period.resultingServiceNumbers.length), ", period ", uint2str(period.date)))); | |
} | |
} else { | |
for (uint insertionIndex = 0; insertionIndex < period.resultingServiceNumbers.length; insertionIndex++) { | |
if (period.resultingServiceNumbers[insertionIndex] == 0) { | |
period.resultingServiceNumbers[insertionIndex] = approvedPlace.serviceNumber; | |
break; | |
} | |
} | |
} | |
} | |
} | |
isFinalized = true; | |
} | |
function getRequest(SeasonSnapshot seasonSnapshot, uint64 index) private view returns (Types.DistributorRequest memory) { | |
(bytes30 serviceNumber, bytes16 userId, Types.DeclarantType declarantType, uint64[] memory fairIds, uint64[] memory periods, uint64[] memory statusCodes, Types.Speciality speciality) = seasonSnapshot.getLeRequest(index); | |
return Types.DistributorRequest(serviceNumber, userId, declarantType, fairIds, periods, statusCodes, speciality); | |
} | |
function uint2str(uint _i) internal pure returns (string memory _uintAsString) { | |
if (_i == 0) { | |
return "0"; | |
} | |
uint j = _i; | |
uint len; | |
while (j != 0) { | |
len++; | |
j /= 10; | |
} | |
bytes memory bstr = new bytes(len); | |
uint k = len - 1; | |
while (_i != 0) { | |
bstr[k--] = byte(uint8(48 + _i % 10)); | |
_i /= 10; | |
} | |
return string(bstr); | |
} | |
} | |
// Команда на распределение. Важный момент: она используется для _Всех_ распределений, включая еженедельные. Команда передаваемая ДИТом на еженедельное | |
// распределение только триггерит запуск распределения, информация берется из первичного. Причина - они передают мусорные данные, а правильные места у нас уже сохранены в первый раз | |
contract DistributionCommand is Owned { | |
uint64[] fairsIds; | |
uint64[] periods; | |
Types.Speciality[] specialities; | |
uint64[] individualsPlacesCount; | |
uint64[] farmersPlacesCount; | |
uint64[] lePlacesCount; | |
uint64[] minPlaces; | |
constructor(uint64[] memory fairsIds_, | |
uint64[] memory periods_, | |
Types.Speciality[] memory specialities_, | |
uint64[] memory individualsPlacesCount_, | |
uint64[] memory farmersPlacesCount_, | |
uint64[] memory lePlacesCount_, | |
uint64[] memory minPlaces_) public { | |
fairsIds = fairsIds_; | |
periods = periods_; | |
specialities = specialities_; | |
individualsPlacesCount = individualsPlacesCount_; | |
farmersPlacesCount = farmersPlacesCount_; | |
lePlacesCount = lePlacesCount_; | |
minPlaces = minPlaces_; | |
} | |
function getValues() public view returns (uint64[] memory, | |
uint64[] memory, | |
Types.Speciality[] memory, | |
uint64[] memory, | |
uint64[] memory, | |
uint64[] memory, | |
uint64[] memory) { | |
return (fairsIds, periods, specialities, individualsPlacesCount, farmersPlacesCount, lePlacesCount, minPlaces); | |
} | |
function uint2str(uint _i) internal pure returns (string memory _uintAsString) { | |
if (_i == 0) { | |
return "0"; | |
} | |
uint j = _i; | |
uint len; | |
while (j != 0) { | |
len++; | |
j /= 10; | |
} | |
bytes memory bstr = new bytes(len); | |
uint k = len - 1; | |
while (_i != 0) { | |
bstr[k--] = byte(uint8(48 + _i % 10)); | |
_i /= 10; | |
} | |
return string(bstr); | |
} | |
} | |
// Агрегатор трёх видов распределения. Появился из-за того, что когда алгоритм стал слишком большим код перестал деплоиться, пришлось разбить на три поменьше. | |
contract DistributionManager is Owned { | |
address public individualsDistributorAddress; | |
address public farmersDistributorAddress; | |
address public leDistributorAddress; | |
address public distributionCommandAddress; | |
address public previousDistributionManager; | |
address public seasonSnapshotAddress; | |
Types.DistributionType public distributionType; | |
bool public isPredistributed; | |
bool public isDistributed; | |
bool isInitialiyRedistributed; | |
uint64 currentPredistributionPosition; | |
uint initStep = 0; | |
RedistributionInfo[] redistributionInfos; | |
uint64[] minPlaces; | |
constructor(address individualsDistributorAddress_, | |
address farmersDistributorAddress_, | |
address leDistributorAddress_, | |
address distributionCommandAddress_, | |
Types.DistributionType distributionType_, | |
address previousDistributionManager_, | |
address seasonSnapshotAddress_) public { | |
individualsDistributorAddress = individualsDistributorAddress_; | |
farmersDistributorAddress = farmersDistributorAddress_; | |
leDistributorAddress = leDistributorAddress_; | |
distributionCommandAddress = distributionCommandAddress_; | |
seasonSnapshotAddress = seasonSnapshotAddress_; | |
distributionType = distributionType_; | |
if (distributionType == Types.DistributionType.Weekly) { | |
DistributionManager previousManager = DistributionManager(previousDistributionManager_); | |
require (previousManager.isDistributed(), "Previous distribution is not distributed!"); | |
previousDistributionManager = previousDistributionManager_; | |
} | |
} | |
function isLoaded() public view returns (bool) { | |
(IDistributor individualsDistributor, IDistributor farmersDistributor, IDistributor leDistributor) = getDistributors(); | |
return individualsDistributor.isLoaded() && farmersDistributor.isLoaded() && leDistributor.isLoaded(); | |
} | |
function isInited() public view returns (bool) { | |
return initStep > 3; | |
} | |
function init() public onlyOrigin { | |
require (!isInited(), "Already inited"); | |
(IDistributor individualsDistributor, IDistributor farmersDistributor, IDistributor leDistributor) = getDistributors(); | |
DistributionCommand distributionCommand = DistributionCommand(distributionCommandAddress); | |
(uint64[] memory fairsIds, uint64[] memory periods, Types.Speciality[] memory specialities, uint64[] memory individualsPlacesCount, uint64[] memory farmersPlacesCount, uint64[] memory lePlacesCount, uint64[] memory minPlaces_) = distributionCommand.getValues(); | |
if (initStep == 0) { | |
individualsDistributor.init(fairsIds, periods, specialities, individualsPlacesCount); | |
} else if (initStep == 1) { | |
farmersDistributor.init(fairsIds, periods, specialities, farmersPlacesCount); | |
} else if (initStep == 2) { | |
leDistributor.init(fairsIds, periods, specialities, lePlacesCount); | |
} else if (initStep == 3) { | |
redistributionInfos.length = periods.length; | |
minPlaces = minPlaces_; | |
} else { | |
require(false, "Unreachable"); | |
} | |
initStep += 1; | |
} | |
function loadRequests() public onlyOwner { | |
require(isInited(), "Is not inited"); | |
require(!isLoaded(), "Already loaded"); | |
(IDistributor individualsDistributor, IDistributor farmersDistributor, IDistributor leDistributor) = getDistributors(); | |
if (!individualsDistributor.isLoaded()) { | |
individualsDistributor.loadRequests(); | |
} | |
else if (!farmersDistributor.isLoaded()) { | |
farmersDistributor.loadRequests(); | |
} | |
else if (!leDistributor.isLoaded()) { | |
leDistributor.loadRequests(); | |
} | |
} | |
function predistribute() private { | |
require(isLoaded(), "Is not loaded"); | |
require(!isPredistributed, "Aleady predistributed"); | |
if (distributionType == Types.DistributionType.Initial) { | |
isPredistributed = true; | |
return; | |
} | |
DistributionManager previousManager = DistributionManager(previousDistributionManager); | |
(IDistributor individualsDistributor, IDistributor farmersDistributor, IDistributor leDistributor) = getDistributors(); | |
uint64 batchSize = 50; | |
uint64 periodsCount = previousManager.getPeriodsCount(); | |
uint64 diff = periodsCount - currentPredistributionPosition; | |
uint64 iterCount = diff > batchSize ? batchSize : diff; | |
for (uint64 i = 0; i < iterCount; i++) { | |
predistributeIndividual(previousManager, individualsDistributor, currentPredistributionPosition); | |
predistributeFarmer(previousManager, farmersDistributor, currentPredistributionPosition); | |
predistributeLe(previousManager, leDistributor, currentPredistributionPosition); | |
currentPredistributionPosition++; | |
} | |
if (currentPredistributionPosition == periodsCount) { | |
isPredistributed = true; | |
} | |
} | |
function predistributeIndividual(DistributionManager previousManager, IDistributor distributor, uint64 index) private { | |
(uint64 fairId, uint64 date, bytes30[] memory serviceNumbers, , , Types.Speciality speciality) = previousManager.getIndividualsPeriod(index); | |
distributor.tryApprove(fairId, date, speciality, serviceNumbers); | |
} | |
function predistributeFarmer(DistributionManager previousManager, IDistributor distributor, uint64 index) private { | |
(uint64 fairId, uint64 date, bytes30[] memory serviceNumbers, , , Types.Speciality speciality) = previousManager.getFarmersPeriod(index); | |
distributor.tryApprove(fairId, date, speciality, serviceNumbers); | |
} | |
function predistributeLe(DistributionManager previousManager, IDistributor distributor, uint64 index) private { | |
(uint64 fairId, uint64 date, bytes30[] memory serviceNumbers, , , Types.Speciality speciality) = previousManager.getLesPeriod(index); | |
distributor.tryApprove(fairId, date, speciality, serviceNumbers); | |
} | |
function distribute() public onlyOwner { | |
require(!isDistributed, "Aleady distributed"); | |
(IDistributor individualsDistributor, IDistributor farmersDistributor, IDistributor leDistributor) = getDistributors(); | |
if (!isInitialiyRedistributed) { | |
if (distributionType == Types.DistributionType.Initial) { | |
// При первом запуске распределения происходит перераспределение мест с тех ярмарок, где количество заявок меньше общего количества мест. | |
performInitialRedistribution(individualsDistributor, farmersDistributor, leDistributor); | |
} else { | |
setPlacesFromPreviousDistribution(individualsDistributor, farmersDistributor, leDistributor); | |
} | |
isInitialiyRedistributed = true; | |
} | |
else if (!isPredistributed) { | |
predistribute(); | |
} | |
else if (!individualsDistributor.isDistributed()) { | |
individualsDistributor.distribute(); | |
} | |
else if (!farmersDistributor.isDistributed()) { | |
farmersDistributor.distribute(); | |
} | |
else if (!leDistributor.isDistributed()) { | |
leDistributor.distribute(); | |
} else { | |
// по окончанию распределения смотрим, есть ли периоды с оставшимися свободными местами. Если есть - то перераспределяем в пользу нуждающихся и запускаем весь алгоритм с начала | |
bool needsRedistribution = false; | |
(,uint64[] memory iPlaces, bool[] memory iAreUpdatables) = individualsDistributor.getPlaces(); | |
(,uint64[] memory fPlaces, bool[] memory fAreUpdatables) = farmersDistributor.getPlaces(); | |
(,uint64[] memory lePlaces, bool[] memory leAreUpdatables) = leDistributor.getPlaces(); | |
if (distributionType == Types.DistributionType.Initial) { // don't redistribute for weekly | |
for (uint i = 0; i < iPlaces.length; i++) { | |
uint64 unoccupiedPlacesCount; | |
if (leAreUpdatables[i] && lePlaces[i] > 0) { // 4.1 | |
unoccupiedPlacesCount = lePlaces[i]; | |
lePlaces[i] = 0; | |
if (redistributionInfos[i].redistributedToIndividualsCount < 2) { | |
fPlaces[i] += unoccupiedPlacesCount; | |
needsRedistribution = true; | |
redistributionInfos[i].redistributedToFarmersCount += 1; | |
} else if (redistributionInfos[i].redistributedToFarmersCount < 2) { | |
iPlaces[i] += unoccupiedPlacesCount; | |
needsRedistribution = true; | |
redistributionInfos[i].redistributedToIndividualsCount += 1; | |
} | |
} else if (iAreUpdatables[i] && iPlaces[i] > 0) { // 4.3 | |
unoccupiedPlacesCount = iPlaces[i]; | |
iPlaces[i] = 0; | |
if (redistributionInfos[i].redistributedToFarmersCount < 2) { | |
fPlaces[i] += unoccupiedPlacesCount; | |
needsRedistribution = true; | |
redistributionInfos[i].redistributedToFarmersCount += 1; | |
} else if (redistributionInfos[i].redistributedToLEsCount < 2) { | |
lePlaces[i] += unoccupiedPlacesCount; | |
needsRedistribution = true; | |
redistributionInfos[i].redistributedToLEsCount += 1; | |
} | |
} else if (fAreUpdatables[i] && fPlaces[i] > 0) { // 4.2 | |
unoccupiedPlacesCount = fPlaces[i]; | |
fPlaces[i] = 0; | |
if (redistributionInfos[i].redistributedToIndividualsCount < 2) { | |
lePlaces[i] += unoccupiedPlacesCount; | |
needsRedistribution = true; | |
redistributionInfos[i].redistributedToLEsCount += 1; | |
} else if (redistributionInfos[i].redistributedToLEsCount < 2) { | |
iPlaces[i] += unoccupiedPlacesCount; | |
needsRedistribution = true; | |
redistributionInfos[i].redistributedToIndividualsCount += 1; | |
} | |
} | |
} | |
} | |
if (needsRedistribution) { | |
// Выполняем перенос мест между видами заявителей | |
individualsDistributor.updatePlaces(iPlaces); | |
farmersDistributor.updatePlaces(fPlaces); | |
leDistributor.updatePlaces(lePlaces); | |
} | |
else { | |
// все заявки которые не получили места и не отклонены отправляются в листы ожидания | |
individualsDistributor.finalize(distributionType); | |
farmersDistributor.finalize(distributionType); | |
leDistributor.finalize(distributionType); | |
isDistributed = true; | |
} | |
} | |
} | |
function uint2str(uint _i) internal pure returns (string memory _uintAsString) { | |
if (_i == 0) { | |
return "0"; | |
} | |
uint j = _i; | |
uint len; | |
while (j != 0) { | |
len++; | |
j /= 10; | |
} | |
bytes memory bstr = new bytes(len); | |
uint k = len - 1; | |
while (_i != 0) { | |
bstr[k--] = byte(uint8(48 + _i % 10)); | |
_i /= 10; | |
} | |
return string(bstr); | |
} | |
function getPeriodsCount() public view returns(uint64) { | |
(IDistributor individualsDistributor, IDistributor farmersDistributor, IDistributor leDistributor) = getDistributors(); | |
uint64 iCount = individualsDistributor.getPeriodsCount(); | |
uint64 fCount = farmersDistributor.getPeriodsCount(); | |
uint64 leCount = leDistributor.getPeriodsCount(); | |
require (iCount == fCount && fCount == leCount); | |
return iCount; | |
} | |
function getIndividualsPeriod(uint64 index) public view returns (uint64 fairId, uint64 date, bytes30[] memory serviceNumbers, bytes30[] memory waitingList, bytes30[] memory rejectedServiceNumbers, Types.Speciality speciality) { | |
IDistributor distributor = IDistributor(individualsDistributorAddress); | |
return distributor.getPeriod(index); | |
} | |
function getFarmersPeriod(uint64 index) public view returns (uint64 fairId, uint64 date, bytes30[] memory serviceNumbers, bytes30[] memory waitingList, bytes30[] memory rejectedServiceNumbers, Types.Speciality speciality) { | |
IDistributor distributor = IDistributor(farmersDistributorAddress); | |
return distributor.getPeriod(index); | |
} | |
function getLesPeriod(uint64 index) public view returns (uint64 fairId, uint64 date, bytes30[] memory serviceNumbers, bytes30[] memory waitingList, bytes30[] memory rejectedServiceNumbers, Types.Speciality speciality) { | |
IDistributor distributor = IDistributor(leDistributorAddress); | |
return distributor.getPeriod(index); | |
} | |
function getMinPlace(uint index) public view returns (uint64) { | |
return minPlaces[index]; | |
} | |
function getDistributors() private view returns(IDistributor, IDistributor, IDistributor) { | |
IDistributor individualsDistributor = IDistributor(individualsDistributorAddress); | |
IDistributor farmersDistributor = IDistributor(farmersDistributorAddress); | |
IDistributor leDistributor = IDistributor(leDistributorAddress); | |
return (individualsDistributor, farmersDistributor, leDistributor); | |
} | |
function getPreviousDistributors() private view returns(IDistributor, IDistributor, IDistributor) { | |
require(previousDistributionManager != address(0), "Doesn't have previous distribution"); | |
require(distributionType == Types.DistributionType.Weekly, "Is not a weekly distribution"); | |
DistributionManager previousManager = DistributionManager(previousDistributionManager); | |
IDistributor individualsDistributor = IDistributor(previousManager.individualsDistributorAddress()); | |
IDistributor farmersDistributor = IDistributor(previousManager.farmersDistributorAddress()); | |
IDistributor leDistributor = IDistributor(previousManager.leDistributorAddress()); | |
return (individualsDistributor, farmersDistributor, leDistributor); | |
} | |
function performInitialRedistribution(IDistributor individualsDistributor, IDistributor farmersDistributor, IDistributor leDistributor) private { | |
uint64[] memory fRequestedPlaces = farmersDistributor.getRequestedPlaces(); | |
uint64[] memory leRequestedPlaces = leDistributor.getRequestedPlaces(); | |
(,uint64[] memory iPlaces, bool[] memory iAreUpdatables) = individualsDistributor.getPlaces(); | |
(,uint64[] memory fPlaces, ) = farmersDistributor.getPlaces(); | |
(,uint64[] memory lePlaces, ) = leDistributor.getPlaces(); | |
for (uint i = 0; i < iPlaces.length; i++) { | |
if (!iAreUpdatables[i]) { | |
continue; | |
} | |
// redistributing unoccupied farmers places, if any | |
int fDiff = int(fPlaces[i]) - int(fRequestedPlaces[i]); | |
if (fDiff > 0) { | |
fPlaces[i] = fRequestedPlaces[i]; | |
iPlaces[i] += uint64(fDiff); | |
} | |
// redistributing unoccupied le places, if any | |
int leDiff = int(lePlaces[i]) - int(leRequestedPlaces[i]); | |
if (leDiff > 0) { | |
lePlaces[i] = leRequestedPlaces[i]; | |
iPlaces[i] += uint64(leDiff); | |
} | |
} | |
individualsDistributor.updatePlaces(iPlaces); | |
farmersDistributor.updatePlaces(fPlaces); | |
leDistributor.updatePlaces(lePlaces); | |
} | |
function setPlacesFromPreviousDistribution(IDistributor individualsDistributor, IDistributor farmersDistributor, IDistributor leDistributor) private { | |
(IDistributor prevIndividualsDistributor, IDistributor prevFarmersDistributor, IDistributor prevLeDistributor) = getPreviousDistributors(); | |
(uint64[] memory iPlaces, , ) = prevIndividualsDistributor.getPlaces(); | |
(uint64[] memory fPlaces, , ) = prevFarmersDistributor.getPlaces(); | |
(uint64[] memory lePlaces, , ) = prevLeDistributor.getPlaces(); | |
individualsDistributor.updatePlaces(iPlaces); | |
farmersDistributor.updatePlaces(fPlaces); | |
leDistributor.updatePlaces(lePlaces); | |
} | |
struct RedistributionInfo { | |
uint8 redistributedToIndividualsCount; | |
uint8 redistributedToFarmersCount; | |
uint8 redistributedToLEsCount; | |
} | |
} | |
// Хранит факт того, что юзер уже имеет заявку зарегистрированную на этот период (+- 1 день). Используется распределителями КФХ и ИП, ФЛ сюда не смотрит (эмулируем баг СИОПР который не умеет определять ФЛ) | |
contract UserPeriodsStorage is Owned { | |
mapping(uint256 => bytes30) declarantRegisteredRequestForPeriod_; | |
function addDeclarant(uint256 userPeriodId, bytes30 serviceNumber) public onlyOrigin { | |
declarantRegisteredRequestForPeriod_[userPeriodId] = serviceNumber; | |
} | |
function declarantRegisteredRequestForPeriod(bytes16 userId, uint64 periodId) public view returns (bytes30) { | |
bytes30 a = declarantRegisteredRequestForPeriod_[uint256(uint128(userId)) << 128 | uint256(periodId)]; | |
if (a != 0){ | |
return a; | |
} | |
// searching for +-1 day periods. TODO: it's better to store a year week. | |
bytes30 b = declarantRegisteredRequestForPeriod_[uint256(uint128(userId)) << 128 | uint256(periodId - 864000000000)]; | |
if (b != 0) { | |
return b; | |
} | |
return declarantRegisteredRequestForPeriod_[uint256(uint128(userId)) << 128 | uint256(periodId + 864000000000)]; | |
} | |
} | |
library Types { | |
struct StatusUpdate { | |
uint64 responseDate; | |
uint64 statusCode; | |
string note; | |
} | |
enum DeclarantType { | |
Individual, // ФЛ | |
IndividualEntrepreneur, // ИП | |
LegalEntity, // ЮЛ | |
IndividualAsIndividualEntrepreneur, // ФЛ как ЮЛ | |
Farmer // ИП КФХ | |
} | |
struct DistributorRequest { | |
bytes30 serviceNumber; | |
bytes16 userId; | |
Types.DeclarantType declarantType; | |
uint64[] fairIds; | |
uint64[] periods; | |
uint64[] periodStatusCodes; | |
Types.Speciality speciality; | |
} | |
enum RedistributionResult { | |
AllPeriodsAreSet, | |
NeedDistributionRerun | |
} | |
struct RedistributionInfo { | |
bool wasRedistributedToIndividuals; | |
bool wasRedistributedToindividuals; | |
bool wasRedistributedToIELEs; | |
} | |
struct FairPeriod { | |
uint64 fairId; | |
uint64 date; | |
uint64 placesCount; | |
Types.Speciality speciality; | |
ApprovedPlace[] approvedPlaces; | |
bytes30[] resultingServiceNumbers; | |
bytes30[] waitingList; | |
bytes30[] rejectedServiceNumbers; | |
bytes30[] allRequests; | |
} | |
struct ApprovedPlace { | |
bytes30 serviceNumber; | |
OptionU64 number; | |
} | |
struct MaybeUninit { | |
bool isInited; | |
uint value; | |
} | |
struct OptionU64 { | |
bool hasValue; | |
uint64 value; | |
} | |
enum Speciality | |
{ | |
UNUSED, | |
Vegetables, | |
Meat, | |
Fish, | |
FoodStuffs | |
} | |
enum DistributionType | |
{ | |
UNUSED, | |
Initial, | |
Weekly | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment