Skip to content

PurchaseOrder.sol

Records a buyer’s committed purchase of a specific BatchToken. Confirming a PurchaseOrder upgrades the BatchToken’s collateral tier from WAREHOUSED (70% LTV) to COMMITTED (80% LTV) and advances TraceLog to the COMMITTED stage.

Before PO: BatchToken at WAREHOUSED → LTV 70% → max loan $315 on 67.5kg batch
After PO: BatchToken at COMMITTED → LTV 80% → max loan $360 on 67.5kg batch

The PurchaseOrder is the on-chain proof that a buyer has committed. This commitment is what justifies the higher LTV — the coffee is no longer speculative inventory, it is contracted inventory.

struct PurchaseOrder {
uint256 orderId; // Monotonically increasing from 1
uint256 batchTokenId;
address buyerWallet; // EU buyer's wallet (or buyer portal hot wallet)
string buyerOrganisation; // e.g. "Sucafina SA"
uint256 agreedPriceUsdc; // Total USDC agreed for the batch
uint256 createdTimestamp;
uint256 confirmedTimestamp;
POStatus status; // PENDING | CONFIRMED | CANCELLED
}
enum POStatus { PENDING, CONFIRMED, CANCELLED }
mapping(uint256 => PurchaseOrder) public orders; // orderId → PO
mapping(uint256 => uint256) public batchToActiveOrder; // batchTokenId → active orderId. 0 = none
// Create a PO (BUYER_ROLE — buyer portal hot wallet)
function createPurchaseOrder(
uint256 batchTokenId,
address buyerWallet, // ← passed at creation time
string calldata buyerOrganisation,
uint256 agreedPriceUsdc
) external onlyRole(BUYER_ROLE) returns (uint256 orderId);
// Confirm a PO (COOP_ROLE — cooperative accepts the order)
// Calls traceLog.updateStage(batchTokenId, 4) for COMMITTED
function confirmPurchaseOrder(uint256 orderId) external onlyRole(COOP_ROLE);
// Cancel a PO (BUYER_ROLE or COOP_ROLE — within 48h of creation)
function cancelPurchaseOrder(uint256 orderId) external;
// View a PO
function getOrder(uint256 orderId)
external view returns (PurchaseOrder memory);

When confirmPurchaseOrder is called:

  1. PurchaseOrder.statusCONFIRMED, confirmedTimestamp set to block.timestamp
  2. batchToActiveOrder[batchTokenId] deleted (PO is no longer “active”)
  3. traceLog.updateStage(batchTokenId, 4) — stage COMMITTED (4 = TraceLog.Stage.COMMITTED)
  4. BatchToken collateral tier recalculated to COMMITTED LTV (80%)
  5. LendingVault notified — existing loan can be topped up to new LTV if requested
  6. HCS event written: COMMITTED with PO reference and agreed price
CheckRevert if violated
buyerWallet != address(0)"PurchaseOrder: zero buyer wallet"
bytes(buyerOrganisation).length > 0"PurchaseOrder: empty buyer organisation"
agreedPriceUsdc > 0"PurchaseOrder: price must be greater than zero"
batchToken.checkExists(batchTokenId)BatchToken revert
!batchToken.hasActiveLoan(batchTokenId)"PurchaseOrder: batch is locked as collateral"
batchToActiveOrder[batchTokenId] == 0"PurchaseOrder: batch already has an active order"
  • Only BUYER_ROLE or COOP_ROLE can cancel.
  • Must be within 48 hours of createdTimestamp.
  • Only PENDING orders can be cancelled.
  • Deletes batchToActiveOrder mapping entry.
event PurchaseOrderCreated(
uint256 indexed orderId,
uint256 indexed batchTokenId,
address indexed buyerWallet
);
event PurchaseOrderConfirmed(
uint256 indexed orderId,
uint256 indexed batchTokenId,
address indexed buyerWallet
);
event PurchaseOrderCancelled(
uint256 indexed orderId,
uint256 indexed batchTokenId,
address indexed cancelledBy
);

Commodity traders (Sucafina, Olam, Kawacom) access a buyer portal dashboard. From the portal:

  • Browse available COMMITTED or WAREHOUSED BatchTokens by cooperative, grade, and weight
  • View DDS eligibility status per batch
  • Create a PurchaseOrder with agreed USDC price
  • Receive automatic notification when cooperative confirms

The buyer portal calls PurchaseOrder.createPurchaseOrder() via AsiliChain API on behalf of the trader.