State Machines
Purpose
Define the valid states and transitions for the core entities.
Application State Machine
pending → approved (merchant action)
pending → rejected (merchant action)
- No transition out of approved/rejected back to pending — a rejected creator submits a new application rather than resurrecting the old one.
Campaign State Machine
draft → live (merchant publishes)
live → paused (merchant pauses)
paused → live (merchant resumes)
live → ended (merchant ends, or automatic end condition)
- Decided default (2026-08-03 — please confirm):
- Paused: existing approved creators' links keep working — clicks and in-flight sales within the 30-day attribution window are still honored. No new applications are accepted while paused.
- Ended: links stop attributing new clicks/sales immediately. Any click that occurred before the end date is still honored if the resulting sale happens within the standard 30-day window; clicks after the end date attribute nothing.
- Commission rate changes mid-flight (decided default 2026-08-03 — please confirm): changing a campaign's commission rate does not require re-consent from already-approved creators. Each approved creator's existing AffiliateLink keeps the rate that was active when they were approved (locked at approval, not at time of sale — this is slightly different from the Sale-level lock in Business Rules, and worth double-checking these two locking rules don't conflict). New applicants after the change apply at the new rate. This avoids building a re-consent/renegotiation flow for MVP.
Sale State Machine
pending → verified (triggers Commission calculation; commission credited to creator wallet)
pending → disputed / failed
verified → refunded (no clawback — creator commission is never clawed back; see Commission Engine)
- "Verified" = payment completed successfully, set the instant Paddle payment clears.
Payout State Machine
wallet accrues (commission credited per verified sale, not yet paid out)
↓ (wallet balance ≥ $50)
pending → processing (payout triggered)
processing → paid
processing → failed → pending (retry)
- Threshold-based for creators ($50, amount TBD in PKR). There is no merchant payout under the current model — merchants keep their own revenue directly from their own Shopify checkout, and are instead billed by SellVia (via Swich) each billing cycle for the commission + platform fee owed; this billing is not threshold-gated, it runs on the billing cycle schedule regardless of sale size (see Money Flow, Payout Process).
Open Questions
- Whether locking a creator's commission rate "at approval" (this doc) vs. "at time of sale" (Business Rules) needs reconciling into one single rule — flagging for proofread since these were written at different points in the conversation and should say the same thing.
Diagrams
Application
stateDiagram-v2
[*] --> pending
pending --> approved
pending --> rejected
approved --> [*]
rejected --> [*]
Campaign
stateDiagram-v2
[*] --> draft
draft --> live
live --> paused
paused --> live
live --> ended
paused --> ended
ended --> [*]
Sale
stateDiagram-v2
[*] --> pending
pending --> verified
pending --> disputed
pending --> failed
verified --> refunded
refunded --> [*]
Payout
stateDiagram-v2
[*] --> accruing
accruing --> pending: balance crosses $50
pending --> processing
processing --> paid
processing --> failed
failed --> pending: retry
paid --> [*]
Update (2026-08-07): Sale States Reversed + New Billing Cycle State Machine
Sale state machine, updated for external-site tracking (01. Money Flow, reversed):
reported → accepted (merchant's sale report passes acceptance checks, per 04. Fraud Prevention)
reported → rejected (failed verification/fraud check)
accepted → billed (included in a completed merchant billing cycle)
accepted → refunded (merchant reports a refund)
Note the terminology shift: "verified" (meaning SellVia witnessed a direct payment) no longer applies u2014 replaced by "reported" u2192 "accepted," reflecting that SellVia is trusting a merchant's claim, not confirming a transaction it processed itself.
New: Billing Cycle state machine
open (accumulating accepted sales for a merchant)
↓ (cycle end date reached)
pending_charge → charged (merchant's card successfully billed)
pending_charge → failed → retrying → charged / suspended
charged → creator_payouts_released (per 01. Money Flow's \"bill first, then pay\" default)
Payout state machine, updated:
wallet accrues (commission credited only after the corresponding Billing Cycle reaches "charged" —
NOT per-sale-instant anymore, per 01. Money Flow's reversed decision)
↓ (wallet balance ≥ $50)
pending → processing → paid
processing → failed → pending (retry)
Open Questions (Update)
- Exact retry policy for a failed merchant billing charge (how many attempts, over what window, before suspending campaigns) u2014 not yet designed
Update (2026-08-07): Snippet Verification Gate Added to draft → live
Founder-confirmed: a Campaign cannot transition from draft to live until the merchant's tracking snippet is verified installed (01. Money Flow, 05. Payment Flow) — a second gate alongside the existing Paddle-onboarding-complete requirement (08. Business Edge Cases). Verification: SellVia can check for the snippet's presence via a test ping/handshake when the merchant attempts to publish, rather than just trusting they installed it correctly.
Both gates on draft → live now:
- Paddle onboarding complete (08. Business Edge Cases)
- Tracking snippet verified installed (this update)
Neither is optional — a campaign with no way to receive payouts, or no way to have its sales tracked, shouldn't be able to go live regardless of which gate is missing.
Update (2026-08-07): Card Failure Policy CONFIRMED
Confirmed: automatic campaign suspension after 3 failed billing attempts over 3 days (e.g. immediate retry, then +24h, then +48h) — no longer a working default. On the 3rd consecutive failure:
- Merchant's live Campaigns auto-transition to
paused(same mechanism already built for Paddle restriction, 08. Business Edge Cases — reused, not reinvented) - Merchant notified with a clear reason and a way to update their card (02. Frontend Architecture's billing card page)
- BillingCycle stays in
failedstatus, accumulating (not lost) until the merchant resolves it and a retry succeeds - Creators' commission for that cycle remains unpaid until resolved — consistent with bill-first-then-pay (01. Money Flow)
Update (2026-08-23): Campaign State Machine Renamed — Now the Offer State Machine
No entity or transition logic changes — this is a rename only, following 01. Domain Model's 2026-08-23 revision (Campaign merged into Offer, no separate entity). Every "Campaign" reference above (the Campaign State Machine section, its mermaid diagram, the draft→live gates, the pause/end honoring rules, the mid-flight commission-rate-change rule) applies identically to Offer now — read "Campaign" as "Offer" throughout this document.
One gate changes in substance, not just name: the second draft→live gate ("tracking snippet verified installed") is now "Shopify webhook connected and verified" per 05. Payment Flow's 2026-08-23 update (Shopify-only for MVP, replacing the universal snippet). The first gate (payment setup complete) changes from "Paddle onboarding complete" to "Swich billing connected" (confirmed 2026-08-23, replacing the earlier bank-details-on-file working default) — see 05. Payment Flow for the connect flow.
Update (2026-08-23): Dead Clawback Reference and Stale Merchant Payout Language Corrected
Sale State Machine's original code block still said "verified → refunded (triggers clawback per Commission Engine's 14-day rule)" — no such rule exists; commission is never clawed back, per Commission Engine's 2026-08-07 confirmation. Corrected in place above.
Payout State Machine's note on merchants still described merchants riding "Paddle's standard rolling payout schedule" — stale under the current external-tracking model, where merchants never receive a payout from SellVia at all. They keep their own revenue directly from their own Shopify checkout and are instead billed by SellVia via Swich for commission + platform fee owed each cycle. Corrected in place above.