1"""Chapter 45 (case study: payments, Stripe / UPI) deep-dive code.
21. Idempotent payment intents.
32. A double-entry ledger: every movement is balanced entries; balances are derived.
43. Unknown outcomes: pending payments resolved by status checks, not guesses.
54. Reconciliation: our ledger vs the bank's settlement file.
65. Webhooks/callbacks: signed, de-duplicated (see Chapter 24), applied once.
7Run: python3 ch45_payments.py"""
8import hashlib
9import hmac
10from collections import defaultdict
11
12
13# ---- 2. ledger ---------------------------------------------------------------------
14
15class Ledger:
16 """Amounts in paise (integers). Each transaction is a list of (account, amount); they must sum to 0."""
17
18 def __init__(self):
19 self.entries = [] # (txn_id, account, amount)
20 self.txns = set()
21
22 def post(self, txn_id, lines):
23 if txn_id in self.txns:
24 return "duplicate" # idempotent: same txn twice changes nothing
25 if sum(a for _, a in lines) != 0:
26 raise ValueError("unbalanced transaction")
27 self.txns.add(txn_id)
28 for account, amount in lines:
29 self.entries.append((txn_id, account, amount))
30 return "posted"
31
32 def balance(self, account):
33 return sum(a for _, acc, a in self.entries if acc == account)
34
35 def total(self):
36 return sum(a for _, _, a in self.entries)
37
38
39def capture(ledger, pid, amount, fee):
40 """Customer paid `amount` for merchant; we keep `fee`. Money sits in our bank clearing account."""
41 return ledger.post(f"capture:{pid}", [("bank_clearing", amount),
42 ("merchant:bookstore", -(amount - fee)),
43 ("revenue:fees", -fee)])
44
45
46def refund(ledger, pid, amount):
47 """A refund is a NEW balanced transaction, never an edit to the old one."""
48 return ledger.post(f"refund:{pid}", [("merchant:bookstore", amount), ("bank_clearing", -amount)])
49
50
51# ---- 1/3. payment intents ------------------------------------------------------------
52
53class Payments:
54 def __init__(self, ledger):
55 self.ledger = ledger
56 self.by_key, self.intents = {}, {}
57
58 def create(self, idem_key, amount):
59 if idem_key in self.by_key:
60 return self.by_key[idem_key]
61 pid = f"pi_{len(self.intents) + 1}"
62 self.intents[pid] = {"amount": amount, "state": "created"}
63 self.by_key[idem_key] = pid
64 return pid
65
66 def submit(self, pid, bank_response):
67 """bank_response: 'success' | 'failure' | 'timeout'."""
68 it = self.intents[pid]
69 if bank_response == "timeout":
70 it["state"] = "pending" # we don't know: never assume failure or success
71 elif bank_response == "success":
72 self._succeed(pid)
73 else:
74 it["state"] = "failed"
75 return it["state"]
76
77 def resolve_pending(self, pid, status_check):
78 """Ask the bank / network what really happened (status-check API), then settle the state."""
79 it = self.intents[pid]
80 if it["state"] != "pending":
81 return it["state"]
82 if status_check == "success":
83 self._succeed(pid)
84 elif status_check == "failure":
85 it["state"] = "failed"
86 return it["state"] # still pending if the bank doesn't know yet
87
88 def _succeed(self, pid):
89 it = self.intents[pid]
90 if it["state"] != "succeeded":
91 it["state"] = "succeeded"
92 capture(self.ledger, pid, it["amount"], fee=it["amount"] * 2 // 100)
93
94
95# ---- 4. reconciliation -------------------------------------------------------------
96
97def reconcile(ours, bank):
98 """ours/bank: {payment_id: amount}. Returns the breaks to investigate."""
99 breaks = {"missing_at_bank": [], "missing_in_ledger": [], "amount_mismatch": []}
100 for pid, amt in ours.items():
101 if pid not in bank:
102 breaks["missing_at_bank"].append(pid)
103 elif bank[pid] != amt:
104 breaks["amount_mismatch"].append((pid, amt, bank[pid]))
105 for pid in bank:
106 if pid not in ours:
107 breaks["missing_in_ledger"].append(pid)
108 return breaks
109
110
111# ---- 5. webhooks -------------------------------------------------------------------
112
113SECRET = b"whsec_bank"
114
115
116def sign(body, ts):
117 return hmac.new(SECRET, f"{ts}.{body}".encode(), hashlib.sha256).hexdigest()
118
119
120class WebhookHandler:
121 def __init__(self, payments):
122 self.payments, self.seen = payments, set()
123
124 def handle(self, event_id, pid, status, body, ts, sig, now):
125 if abs(now - ts) > 300 or not hmac.compare_digest(sign(body, ts), sig):
126 return 400
127 if event_id in self.seen:
128 return 200 # already applied
129 self.seen.add(event_id)
130 self.payments.resolve_pending(pid, status)
131 return 200
132
133
134# ---- checks ------------------------------------------------------------------------
135
136def run_checks():
137 ledger = Ledger()
138 pay = Payments(ledger)
139
140 # 1. idempotent intent: the app retries "pay ₹1,248"
141 p1 = pay.create("order-881", 124_800)
142 assert pay.create("order-881", 124_800) == p1 and len(pay.intents) == 1
143
144 # 2/3. success posts a balanced capture
145 assert pay.submit(p1, "success") == "succeeded"
146 assert ledger.balance("bank_clearing") == 124_800
147 assert ledger.balance("merchant:bookstore") == -122_304 # owed to merchant (credit)
148 assert ledger.balance("revenue:fees") == -2_496 # 2% fee
149 assert ledger.total() == 0 # always balances
150
151 # the bank times out: state is pending, NOT failed (the customer may have been debited)
152 p2 = pay.create("order-882", 50_000)
153 assert pay.submit(p2, "timeout") == "pending"
154 assert pay.resolve_pending(p2, "unknown") == "pending" # bank doesn't know yet
155 # a signed callback arrives, twice
156 wh = WebhookHandler(pay)
157 body = '{"payment":"pi_2","status":"success"}'
158 sig = sign(body, 1_000)
159 assert wh.handle("evt-9", p2, "success", body, 1_000, sig, now=1_010) == 200
160 assert wh.handle("evt-9", p2, "success", body, 1_000, sig, now=1_020) == 200
161 assert pay.intents[p2]["state"] == "succeeded"
162 assert ledger.balance("bank_clearing") == 174_800 # captured once, not twice
163 assert wh.handle("evt-10", p2, "success", body, 1_000, "forged", now=1_010) == 400
164
165 # refunds are new entries; history is never edited
166 assert refund(ledger, p1, 124_800) == "posted"
167 assert refund(ledger, p1, 124_800) == "duplicate"
168 assert ledger.balance("bank_clearing") == 50_000 and ledger.total() == 0
169 try:
170 ledger.post("bad", [("a", 100), ("b", -90)])
171 assert False
172 except ValueError:
173 pass
174
175 # 4. reconciliation against the bank's settlement file
176 ours = {"pi_1": 124_800, "pi_2": 50_000, "pi_3": 9_900}
177 bank = {"pi_1": 124_800, "pi_2": 5_000, "pi_4": 20_000}
178 breaks = reconcile(ours, bank)
179 assert breaks == {"missing_at_bank": ["pi_3"], "missing_in_ledger": ["pi_4"],
180 "amount_mismatch": [("pi_2", 50_000, 5_000)]}
181
182 # Step 2 arithmetic: UPI-scale (20 billion a month) for perspective, and our assumed 2,000 TPS peak
183 assert round(20e9 / (30 * 86_400)) == 7_716
184 assert 2_000 * 3 == 6_000 # ledger entries a second at peak (3 lines per capture)
185 return {"merchant_balance": ledger.balance("merchant:bookstore"), "breaks": breaks}
186
187
188if __name__ == "__main__":
189 print("All checks pass.", run_checks())