You are building the charge processor at the core of a checkout backend. It is created with no arguments and exposes a single method, charge(ref, amountCents). ref is a string the caller hands in with each charge attempt, and amountCents is the integer amount in cents. The method processes the charge and returns an object with a chargeId, a unique string identifying the charge it created, and the amountCents that was charged.
The catch is the real world this processor lives in. Networks drop responses. A client fires a charge, the charge succeeds on your side, but the response never reaches the client, so the client does the only safe thing it knows: it retries, re-sending the exact same request, ref and all. Real payment clients are built to retry this way, and a busy checkout will see the same ref arrive more than once for a single intended purchase. Your processor decides what that second arrival means.
Specify the processor's behavior completely, and think hardest about that repeat. When a ref the processor has already handled arrives again, decide exactly what charge() must return and what it must do to the charge it created the first time, and write that rule as something a deterministic test could check. A processor that treats every call as a brand new charge will turn one customer's single purchase into two real charges the moment their client retries.
Example
charge('order-9f3', 4200) returns { chargeId: 'ch_001', amountCents: 4200 }. The interesting case is the retry: the client's network dropped the response, so it sends charge('order-9f3', 4200) again. A processor that mints a fresh charge per call returns a new chargeId here, leaving two charges on the books for one purchase.