"""Local marketplace contract, not a Stripe emulator or production webhook.
Run: python3 checkout_prelevement.py
No network, credentials, actual payment, shipment, refund or transfer.
The PSP snapshots below are fictitious inputs, assumed retrieved server-side.
SQLite tests local persistence and sequential replays, not distributed delivery.
"""
from copy import deepcopy
from itertools import permutations
import json,sqlite3

ORDER={'id':'O-1200','session':'cs_demo_active','intent':'pi_demo_active','amount':120000,'currency':'eur','mode':'payment','reserve_until':20,'sellers':{'A':80000,'B':40000}}
PAID={'session':'cs_demo_active','intent':'pi_demo_active','amount':120000,'currency':'eur','mode':'payment','session_status':'complete','payment_status':'paid','intent_status':'succeeded','balance_transaction':True,'available_on_passed':True}
PENDING=dict(PAID,payment_status='unpaid',intent_status='processing')
FAILED=dict(PENDING,intent_status='requires_payment_method')

class Journal:
 def __init__(self):
  self.db=sqlite3.connect(':memory:')
  self.db.executescript('CREATE TABLE received(event_id TEXT PRIMARY KEY, decision TEXT); CREATE TABLE outbox(order_id TEXT,seller TEXT,amount INTEGER,PRIMARY KEY(order_id,seller));')
 def decide(self,event_id,event_type,snapshot,*,now,reservation_rechecked=False):
  # Event names are wake-up signals, not financial facts in this model.
  if self.db.execute('SELECT 1 FROM received WHERE event_id=?',(event_id,)).fetchone():return 'duplicate_event'
  if any(snapshot[k]!=ORDER[k] for k in ['session','intent']):decision='other_attempt'
  elif any(snapshot[k]!=ORDER[k] for k in ['amount','currency','mode']):decision='contract_mismatch'
  elif snapshot['intent_status']=='requires_payment_method':decision='payment_failed'
  elif snapshot['payment_status']!='paid' or snapshot['intent_status']!='succeeded':decision='await_payment'
  elif now>=ORDER['reserve_until'] and not reservation_rechecked:decision='paid_stock_review'
  else:decision='ready'
  with self.db:
   self.db.execute('INSERT INTO received VALUES (?,?)',(event_id,decision))
   if decision=='ready':
    for seller,amount in ORDER['sellers'].items():
     self.db.execute('INSERT OR IGNORE INTO outbox VALUES (?,?,?)',(ORDER['id'],seller,amount))
  return decision
 def effects(self):return self.db.execute('SELECT seller,amount FROM outbox ORDER BY seller').fetchall()

checks=[]
def check(name,condition):
 assert condition,name
 checks.append(name)

j=Journal()
check('completed_still_processing',j.decide('evt_done','checkout.session.completed',PENDING,now=10)=='await_payment')
check('no_fulfillment_while_pending',j.effects()==[])
check('timer_does_not_prove_payment',j.decide('timer_21','timer',PENDING,now=21)=='await_payment')
check('late_success_needs_stock_review',j.decide('evt_paid','checkout.session.async_payment_succeeded',PAID,now=120)=='paid_stock_review')
check('paid_but_stock_unchecked_no_effect',j.effects()==[])
check('stock_rechecked_allows_release',j.decide('stock_review_1','internal_stock_review',PAID,now=121,reservation_rechecked=True)=='ready')
check('two_seller_intentions',j.effects()==[('A',80000),('B',40000)])
check('duplicate_event_detected',j.decide('stock_review_1','internal_stock_review',PAID,now=122,reservation_rechecked=True)=='duplicate_event')
check('different_event_same_business_no_repeat',j.decide('evt_paid_again','checkout.session.async_payment_succeeded',PAID,now=122,reservation_rechecked=True)=='ready' and len(j.effects())==2)
check('stale_completed_uses_current_snapshot',j.decide('evt_old_done','checkout.session.completed',PAID,now=123,reservation_rechecked=True)=='ready' and len(j.effects())==2)
old=dict(FAILED,session='cs_demo_old',intent='pi_demo_old')
check('old_failed_attempt_does_not_cancel_active',j.decide('evt_old_failed','checkout.session.async_payment_failed',old,now=124)=='other_attempt')
check('active_intentions_preserved',len(j.effects())==2)
failed=Journal()
check('active_failure_explained',failed.decide('evt_fail','checkout.session.async_payment_failed',FAILED,now=15)=='payment_failed')
check('no_shipping_for_failure',failed.effects()==[])
for field,value in [('amount',119999),('currency','usd'),('mode','setup')]:
 bad=dict(PAID,**{field:value});q=Journal();check(field+'_mismatch',q.decide('bad_'+field,'checkout.session.async_payment_succeeded',bad,now=10)=='contract_mismatch' and q.effects()==[])
for field,value in [('session','cs_wrong'),('intent','pi_wrong')]:
 bad=dict(PAID,**{field:value});q=Journal();check(field+'_other_attempt',q.decide('bad_'+field,'checkout.session.async_payment_succeeded',bad,now=10)=='other_attempt' and q.effects()==[])
q=Journal();check('no_payment_required_outside_positive_order',q.decide('evt_setup','checkout.session.completed',dict(PAID,payment_status='no_payment_required'),now=10)=='await_payment')
q=Journal();check('inconsistent_session_and_intent',q.decide('evt_inconsistent','checkout.session.async_payment_succeeded',dict(PAID,intent_status='processing'),now=10)=='await_payment')
for now,want in [(19,'ready'),(20,'paid_stock_review'),(21,'paid_stock_review')]:
 q=Journal();check('reservation_boundary_'+str(now),q.decide('edge','payment_recheck',PAID,now=now)==want)
# All notifications are delivered after settlement; each read returns PAID.
for i,events in enumerate(permutations([('e1','checkout.session.completed'),('e2','checkout.session.async_payment_succeeded'),('e3','checkout.session.completed')])):
 q=Journal()
 for event,kind in events:q.decide(event,kind,PAID,now=120,reservation_rechecked=True)
 check('delivery_permutation_'+str(i),q.effects()==[('A',80000),('B',40000)])
commission={seller:amount*125//1000 for seller,amount in ORDER['sellers'].items()}
net={seller:amount-commission[seller] for seller,amount in ORDER['sellers'].items()}
check('commission_A_100_eur',commission['A']==10000)
check('commission_B_50_eur',commission['B']==5000)
check('net_A_700_eur',net['A']==70000)
check('net_B_350_eur',net['B']==35000)
check('allocation_balances',sum(net.values())+sum(commission.values())==120000)
wrong={
 'completed_means_paid':PENDING['session_status']=='complete',
 'balance_exists_means_paid':PENDING['balance_transaction'],
 'available_on_passed_means_paid':PENDING['available_on_passed'],
}
for name,allows in wrong.items():check('refutes_'+name,allows and PENDING['intent_status']!='succeeded')
print(json.dumps({'checks':len(checks),'names':checks,'deliveryPermutations':6,'badShortcuts':list(wrong),'sellerGrossCents':ORDER['sellers'],'commissionCents':commission,'sellerNetCents':net,'effects':j.effects(),'noNetwork':True,'scope':'Fictitious current snapshots; SQLite sequential local intentions only. No Stripe API or signature, bank timing, distributed concurrency, shipment or transfer tested.'},ensure_ascii=False,indent=2))
