EZOM Mateship Logistics Pty Ltd · Payments · SaaS billing

Organisation Subscription · Stripe

Self-serve plans (BASIC, STANDARD, CUSTOM) billed through Stripe, replacing the old wallet-based LEGACY monthly fee.

What it shows
Business rules & states
My role
Fullstack Developer · requirement analysis
When
08/2026 – 09/2026 (EZOM-1538)
Company
EZOM Mateship Logistics Pty Ltd
StripeReactNode.jsGraphQLPostgreSQLJest

01 · The problem

Context

EzOM used to charge a flat monthly fee from each organisation's wallet (LEGACY). The new model sells plans with feature sets and limits on users and warehouses, paid by card through Stripe.

Billing has a lot of states that look alike but mean different things. A payment Stripe has accepted isn't the same as a plan EzOM has applied. A downgrade shouldn't cut access mid-period. A $0 trial invoice marked paid shouldn't flip a trial to ACTIVE. If any of these get mixed up, a customer gets charged twice or loses features they paid for.

02 · What I did

  1. 01Built the plan picker and review screen: current → target plan, effective date, amount due now, credit, tax, and the next renewal amount.
  2. 02Implemented change timing: upgrades and monthly → annual apply immediately; downgrades and annual → monthly are scheduled for period end; a mixed change runs in two phases.
  3. 03Added 14-day trials for new BASIC/STANDARD signups, Buy Now to skip or end a trial, and resuming the confirmation after the user adds a card.
  4. 04Checked usage against the target plan's limits (users, warehouses) and blocked confirmation with a clear ‘reduce by N’ message.
  5. 05Moved feature access to plan entitlements: PAST_DUE keeps the plan, while UNPAID, INCOMPLETE, EXPIRED, and CANCELED fall back to FREE.
  6. 06Used our AI requirement workflow to write the as-is spec of the whole feature for BA/PO sign-off: 22 functional requirements, 71 acceptance criteria, and a list of open questions.

03 · Decisions & trade-offs

Accepted ≠ applied

An immediate change sits in PENDING_PAYMENT until Stripe's confirmation arrives. The UI never says ‘your plan is upgraded’ just because the request was received.

Previews expire after 10 minutes

Confirming an old preview, or one whose subscription changed in the meantime, returns PREVIEW_STALE, so nobody pays a price they didn't see.

One pending change per organisation

While a change is PENDING_PAYMENT or SCHEDULED, new changes are refused (CHANGE_IN_PROGRESS). Repeating the same confirmation returns the stored result instead of calling Stripe again.

Trials survive $0 invoices

A zero-value invoice confirmed before the trial event keeps the subscription in TRIALING, instead of promoting it to ACTIVE because the invoice says ‘paid’.

04 · What I produced

Acceptance criteriaAC-028 · Accepted ≠ applied

Given Stripe accepted an immediate change

but EzOM hasn't recorded it yet

Then the change stays PENDING_PAYMENT;

the target plan is not granted yet

status: HIỆN TRẠNG · CHỜ DUYỆT

unclear cases stay NEEDS_CLARIFICATION

From the as-is spec: 22 functional requirements, 71 acceptance criteria, and 22 open questions.

Business rulesChange timing & access

upgrade · monthly → annual apply immediately

downgrade · annual → monthly scheduled for period end

mixed change runs in two phases

PAST_DUE keeps the plan

UNPAID · INCOMPLETE ·

EXPIRED · CANCELED fall back to FREE

preview older than 10 min PREVIEW_STALE

a change already pending CHANGE_IN_PROGRESS

05 · Before / after

Drag the divider to compare

Demo image (replace): plan picker and review screen
Demo image (replace): legacy wallet-based monthly fee
BeforeAfter
BillingBefore: a flat monthly fee taken from the organisation's wallet. After: the plan picker and review screen with amount due, credit, and next renewal.

06 · What I achieved

What changed

  • Organisations can trial, buy, upgrade, downgrade, cancel, and resume plans without contacting support.
  • LEGACY organisations move to the new plans with the old wallet fee set to 0, so they're never billed twice.
  • Every rule is traceable from requirement to acceptance criterion to test case, with undecided cases marked NEEDS_CLARIFICATION instead of guessed.

What I'd keep doing

  • “Write states down before writing code. Most billing bugs come from two states being treated as one.”
  • “Never let the UI promise what the payment provider hasn't confirmed yet.”

Next case study · EZOM

Monthly Invoices & Statements · Xero

Organisation Subscription · Stripe · case study | Bui Thuy Ngoc