This document describes the lifecycle of an invoice in the QuickLendX protocol, from creation through settlement or default. Audience: contributors and downstream integrators who need to understand how invoice state transitions work on-chain.
┌──────────┐
│ Pending │ ◄─── invoice created by a verified business
└────┬─────┘
│ verify_invoice(admin, invoice_id)
▼
┌──────────┐
│ Verified │ ◄─── KYC-checked, open for investor bids
└────┬─────┘
│ accept_bid(business, invoice_id, bid_id)
▼
┌────────┐
│ Funded │ ◄─── escrow locked; repayment window running
└───┬────┘
┌──────────┴──────────┐
▼ ▼
┌──────────┐ ┌────────────┐
│ Paid │ │ Defaulted │
└──────────┘ └────────────┘
Before Funded, the invoice may also reach a terminal state via:
cancel_invoice → Cancelled
resolve_dispute → Refunded (Funded invoices only)
Terminal states (Paid, Defaulted, Cancelled, Refunded) are irreversible.
| Status | Terminal? | Description |
|---|---|---|
Pending |
No | Submitted; awaiting admin verification. |
Verified |
No | Verified by admin; investor bids may now be placed. |
Funded |
No | Bid accepted; funds held in escrow. |
Paid |
Yes | Repaid by business; escrow released to investor less fees. |
Defaulted |
Yes | Grace period elapsed without repayment; escrow distributed. |
Cancelled |
Yes | Cancelled before funding by business or admin. |
Refunded |
Yes | Funded invoice refunded following a resolved dispute. |
Source: InvoiceStatus in quicklendx-contracts/src/types.rs.
contract.verify_invoice(env, admin: Address, invoice_id: BytesN<32>)
-> Result<(), QuickLendXError>- Caller: admin (
AdminStorage::require_adminenforced). - Precondition: invoice exists and is
Pending. - Effect:
status = Verified; emitsInvoiceVerifiedevent.
contract.accept_bid(env, caller: Address, invoice_id: BytesN<32>, bid_id: BytesN<32>)
-> Result<(), QuickLendXError>- Caller: business owner of the invoice.
- Precondition: invoice is
Verified; chosen bid isPlaced; business is KYC-verified. - Effect: bid amount transferred into escrow;
status = Funded; bid markedAccepted; all other bids on this invoice markedCancelled.
contract.settle_invoice(
env, caller: Address, invoice_id: BytesN<32>,
payment_token: Address, payment_amount: i128,
) -> Result<(), QuickLendXError>- Caller: business owner or admin.
- Precondition: invoice is
Funded;payment_amount >= invoice.amount. - Effect: escrow released to investor (minus platform fee);
status = Paid; payment recorded.
contract.trigger_default(env, caller: Address, invoice_id: BytesN<32>)
-> Result<(), QuickLendXError>- Caller: permissionless (anyone may trigger after the deadline).
- Precondition: invoice is
Funded;now > due_date + grace_period_seconds. - Effect: escrow distributed per default-finality policy;
status = Defaulted.
contract.cancel_invoice(env, caller: Address, invoice_id: BytesN<32>)
-> Result<(), QuickLendXError>- Caller: business owner or admin.
- Precondition: invoice is
PendingorVerified(not yet funded). - Effect:
status = Cancelled; outstanding bids markedWithdrawn.
contract.resolve_dispute(env, admin: Address, dispute_id: BytesN<32>, resolution: DisputeResolution)
-> Result<(), QuickLendXError>- Caller: admin.
- Precondition: dispute is
UnderReview; underlying invoice isFunded. - Effect (when
resolution == Refund): escrow returned to investor;status = Refunded.
-
Terminal states are final. No entrypoint may change the status of an invoice that is
Paid,Defaulted,Cancelled, orRefunded. Enforced insrc/invariants.rs. -
Escrow is atomic with funding. An invoice is
Fundediff a live escrow record exists.settle_invoiceandtrigger_defaultboth drain the escrow in the same ledger transaction. -
Exactly one accepted bid per invoice.
accept_bidatomically accepts one bid and cancels the rest; a second call on a funded invoice is rejected withInvoiceAlreadyFunded(1002). -
KYC gating at submission.
store_invoicechecksBusinessVerificationStorageand rejects unverified callers withBusinessNotVerified(1600).
store_invoice and update_invoice_metadata maintain four secondary indexes:
| Index storage key | Indexed field | Query entrypoint |
|---|---|---|
inv_bus:{address} |
business_owner |
get_invoices_by_customer |
inv_tax:{tax_id} |
tax_id |
get_invoices_by_tax_id |
inv_tag:{tag} |
each tag in tags |
get_invoices_by_tag |
inv_cat:{category} |
category |
get_invoices_by_category |
If indexes drift (e.g. after a backup restore), rebuild them with:
contract.rebuild_invoice_indexes(env, admin, offset: u32, limit: u32)
-> Result<RebuildReport, QuickLendXError>The call is paginated, idempotent, and resumable — run it in pages using
report.next_offset until report.next_offset == total_invoices.
| Error | Code | Raised when |
|---|---|---|
InvoiceNotFound |
1000 | Invoice ID absent from storage. |
InvoiceNotAvailableForFunding |
1001 | Funding attempted on a non-Verified invoice. |
InvoiceAlreadyFunded |
1002 | accept_bid called on an already-funded invoice. |
InvoiceAmountInvalid |
1003 | Amount below min_invoice_amount. |
InvoiceDueDateInvalid |
1004 | Due date exceeds max_due_date_days from now. |
InvoiceNotFunded |
1005 | Settlement attempted on non-Funded invoice. |
InvoiceAlreadyDefaulted |
1006 | Default triggered on already-defaulted invoice. |
InvoiceFrozen |
1007 | Operation blocked on administratively frozen invoice. |
Full error reference: docs/ERROR_CODES.md.
docs/ESCROW.md— escrow lifecycle and release conditions.docs/DISPUTE.md— dispute open / review / resolve flow.docs/STORAGE_LAYOUT.md— on-chain storage key layout.docs/QUERIES.md— read-only query entrypoints.docs/ERROR_CODES.md— complete typed error reference.docs/INVOICE_LOCK.md— lock duration, admin freeze auto-release, and escrow hold timing.