import csv
from decimal import Decimal
from pathlib import Path

BASE = Path(__file__).resolve().parent
with (BASE / 'source_rows.csv').open(newline='', encoding='utf-8') as f:
    rows = list(csv.DictReader(f))
D = lambda x: Decimal(x.strip() or '0')
by_id = {r['source_id']: r for r in rows}
assert len(rows) == len(by_id), 'source IDs must be unique'
# Confirmed export representation collapse: same posting_id is one export posting.
assert by_id['G5a']['posting_id'] == by_id['G5b']['posting_id']
assert D(by_id['G5a']['signed_cash_cny']) == D(by_id['G5b']['signed_cash_cny']) == 0
# G2 has two distinct actual postings, not a repeated export row.
assert by_id['G2a']['posting_id'] != by_id['G2b']['posting_id']
assert by_id['G2a']['ledger_voucher_id'] == by_id['G2b']['ledger_voucher_id']
assert D(by_id['G2a']['signed_cash_cny']) == D(by_id['G2b']['signed_cash_cny']) == 1200

# Supplied period-end control balances; no opening balances/full population supplied.
book_control = Decimal('52800')
bank_control = Decimal('50410')
# Matches: R8 split exactly allocates the single G2 receipt; D7/C4 timing,
# not yet-cleared transactions. Bank+ledger amounts shown explicitly.
r8 = D(by_id['R8a']['signed_cash_cny']) + D(by_id['R8b']['signed_cash_cny'])
g2_receipt = D(by_id['G2a']['signed_cash_cny'])
assert r8 == g2_receipt, 'R8 allocations must exactly equal the G2 receipt'
assert D(by_id['D7']['signed_cash_cny']) == 1800
assert D(by_id['C4']['signed_cash_cny']) == -500

# Proposed corrections in signed cash effect: source records drive amount.
# offset accounts are deliberately unresolved (not supplied by evidence).
journals = [
 {'source_ids':'G2b','debit':'UNRESOLVED original offset account','credit':'Cash','amount':abs(D(by_id['G2b']['signed_cash_cny'])),'period':'July','status':'Proposed/unposted; conditional on original offset lookup','cash_effect':-abs(D(by_id['G2b']['signed_cash_cny']))},
 {'source_ids':'G3','debit':'Cash','credit':'UNRESOLVED original offset account','amount':Decimal('-670') - D(by_id['G3']['signed_cash_cny']),'period':'July','status':'Proposed/unposted; conditional on original offset lookup','cash_effect':Decimal('-670') - D(by_id['G3']['signed_cash_cny'])},
 {'source_ids':'FEE','debit':'UNRESOLVED fee offset account','credit':'Cash','amount':abs(D(by_id['FEE']['signed_cash_cny'])),'period':'July','status':'Proposed/unposted; conditional on bank support and account lookup','cash_effect':-abs(D(by_id['FEE']['signed_cash_cny']))},
 {'source_ids':'INT','debit':'Cash','credit':'UNRESOLVED interest offset account','amount':D(by_id['INT']['signed_cash_cny']),'period':'July','status':'Proposed/unposted; conditional on bank support and account lookup','cash_effect':D(by_id['INT']['signed_cash_cny'])},
]
# G3 correction = supported signed movement (-670) minus recorded (-760) = +90.
assert journals[1]['amount'] == Decimal('90')
assert journals[0]['cash_effect'] == -1200
book_adjustment = sum((j['cash_effect'] for j in journals), Decimal(0))
assert book_adjustment == Decimal('-1150')
adjusted_book = book_control + book_adjustment
# Bank-side timing bridge uses only supported items already recorded in books.
timing = D(by_id['D7']['signed_cash_cny']) + D(by_id['C4']['signed_cash_cny'])
adjusted_bank = bank_control + timing
residual = adjusted_bank - adjusted_book
assert adjusted_book == Decimal('51650')
assert adjusted_bank == Decimal('51710')
assert residual == Decimal('60')
# Journal cash effects and book adjustment bridge are generated from the same rows.
assert sum((j['cash_effect'] for j in journals), Decimal(0)) == adjusted_book - book_control
# Unknown debit must not enter a supported journal or timing bridge.
assert all('UNK' not in j['source_ids'] for j in journals)
assert D(by_id['UNK']['signed_cash_cny']) == Decimal('-100')

def money(x): return f'{x:,.0f}'
print('EXECUTED CASH RECONCILIATION — CNY; signed cash movement (+ increases cash)')
print('Controls: GL 52,800 (supplied); bank 50,410 (supplied); cutoff July (per assignment)')
print('\nMatching allocations')
print('Group | Ledger IDs / amount | Bank IDs / amount | Result')
print(f'G2 / R8 | G2a + G2b: {money(g2_receipt)} + {money(g2_receipt)} recorded; duplicate G2b reversed | R8a {money(D(by_id["R8a"]["signed_cash_cny"]))} + R8b {money(D(by_id["R8b"]["signed_cash_cny"]))} = {money(r8)} | R8 uniquely allocates one receipt; G2b separate duplicate posting')
print('G3 | G3 recorded cash -760 | bank actual -670 (per supplied fact; bank transaction ID not supplied) | correction +90')
print('G5 | G5a/G5b same POST-G5 export representation; zero incremental movements | n/a | collapse representation only; do not reverse posting')
print('\nBook adjustment journal rows (proposed, unposted)')
print('Source IDs | Debit | Credit | Amount | Cash effect | Period/status')
for j in journals:
    print(f"{j['source_ids']} | {j['debit']} | {j['credit']} | {money(j['amount'])} | {money(j['cash_effect'])} | {j['period']}; {j['status']}")
print(f'Book adjustment total: {money(book_adjustment)}; adjusted book: {money(adjusted_book)}')
print('\nBank timing bridge (supported, already in books)')
print('D7 deposit in transit +1,800; C4 outstanding check -500')
print(f'Timing subtotal: {money(timing)}; adjusted bank: {money(adjusted_bank)}')
print(f'Adjusted bank less adjusted book residual: +{money(residual)}')
print('FEE -60 and INT +20 are in bank control and journal proposals, not timing; UNK -100 is in bank control, unresolved, and excluded from journals.')
print('CONTROL LIMIT: no opening balances or full-period movement population supplied; period-end totals are supplied external-balance controls, not independently rebuilt from complete movements.')
