Explore Payments and Lifecycle Events#

A payment is more than one row: it belongs to a small financial world and produces events as it moves through its payment rail. This tutorial explores those connected records.

1. Generate a payment world#

The bundled configuration creates customers, accounts, cards, merchants, devices, payments, events, and ledger entries in memory.

import polars as pl

import fraudtwin

data = fraudtwin.generate()
print("Run:", data.run_id)
Run: RUN-19652188a1efbe6c

2. Explore the financial world#

Entities are connected by stable IDs. For example, each account belongs to a customer and an institution.

accounts = pl.DataFrame(
    [
        {
            "Account": account.account_id,
            "Customer": account.customer_id,
            "Institution": account.institution_id,
            "Status": account.status,
            "Balance": account.ledger_balance,
        }
        for account in data.entities.accounts[:5]
    ]
)

accounts
shape: (5, 5)
AccountCustomerInstitutionStatusBalance
strstrstrstrf64
"ACC-000001""CUS-000009""INS-000001""ACTIVE"24352.3
"ACC-000002""CUS-000007""INS-000003""ACTIVE"17560.38
"ACC-000003""CUS-000010""INS-000001""ACTIVE"6307.11
"ACC-000004""CUS-000007""INS-000001""ACTIVE"13689.01
"ACC-000005""CUS-000004""INS-000001""ACTIVE"15615.43

3. Inspect payments#

Payments contain the business-level information: rail, amount, payer, payee or merchant, and current status.

payments = pl.DataFrame(
    [
        {
            "Payment": payment.payment_id,
            "Rail": payment.payment_rail,
            "Amount": payment.amount,
            "Type": payment.payment_type,
            "Status": payment.current_status,
        }
        for payment in data.behavior.payments[:5]
    ]
)

payments
shape: (5, 5)
PaymentRailAmountTypeStatus
strstrf64strstr
"PAY-00000001""ACCOUNT_TRANSFER"15.29"TRANSFER""COMPLETED"
"PAY-00000002""PIX"21.7"TRANSFER""RECEIVED"
"PAY-00000003""CARD"15.63"PURCHASE""SETTLED"
"PAY-00000004""CARD"29.24"PURCHASE""SETTLED"
"PAY-00000005""PIX"24.08"TRANSFER""RECEIVED"

4. Follow one payment through its lifecycle#

A card payment produces a causally ordered chain of events. The event timestamps show when each step occurred.

card_payment = next(payment for payment in data.behavior.payments if payment.payment_rail == "CARD")

lifecycle = pl.DataFrame(
    [
        {
            "Event": event.event_type,
            "Event time": event.event_time,
            "Available at": event.source_available_at,
        }
        for event in data.behavior.payment_events
        if event.payment_id == card_payment.payment_id
    ]
).sort("Event time")

print("Payment:", card_payment.payment_id)
lifecycle
Payment: PAY-00000003
shape: (6, 3)
EventEvent timeAvailable at
strdatetime[μs, UTC]datetime[μs, UTC]
"CARD_PAYMENT_INITIATED"2026-01-01 12:21:00 UTC2026-01-01 12:21:05 UTC
"CARD_AUTHORIZATION_REQUESTED"2026-01-01 12:21:01 UTC2026-01-01 12:21:06 UTC
"CARD_AUTHORIZED"2026-01-01 12:21:02 UTC2026-01-01 12:21:07 UTC
"CARD_CAPTURED"2026-01-01 12:21:07 UTC2026-01-01 12:21:12 UTC
"CARD_CLEARED"2026-01-01 12:21:37 UTC2026-01-01 12:21:42 UTC
"CARD_SETTLED"2026-01-01 12:22:37 UTC2026-01-01 12:22:42 UTC

5. Inspect the ledger impact#

Ledger entries connect financial effects back to both a payment and its event. A payment may have no posted entry until its lifecycle reaches the appropriate state.

ledger_payment_id = data.behavior.ledger_entries[0].payment_id
ledger = pl.DataFrame(
    [
        {
            "Entry": entry.ledger_entry_id,
            "Payment": entry.payment_id,
            "Account": entry.account_id,
            "Type": entry.entry_type,
            "Amount": entry.amount,
            "Balance after": entry.balance_after,
        }
        for entry in data.behavior.ledger_entries
        if entry.payment_id == ledger_payment_id
    ]
)

ledger
shape: (2, 6)
EntryPaymentAccountTypeAmountBalance after
strstrstrstrf64f64
"LED-EVT-00000017-01""PAY-00000017""ACC-000003""DEBIT"7.546299.57
"LED-EVT-00000017-02""PAY-00000017""ACC-000011""CREDIT"7.5413883.21

You have now followed the main relationships in a generated payment world: entities create context, payments represent business actions, events describe their lifecycles, and ledger entries record their financial effects.

Record the generated shape and tutorial contract.#

summary = {
    "payments": len(data.behavior.payments),
    "events": len(data.behavior.payment_events),
}
print(summary)
assert summary["payments"] >= 0
{'payments': 100, 'events': 433}

Inspect stable payment identities.#

ids = [item.payment_id for item in data.behavior.payments]
assert len(ids) == len(set(ids))
print({"unique_payment_ids": len(ids)})
{'unique_payment_ids': 100}

Compare event-time coverage.#

times = [event.event_time for event in data.behavior.payment_events]
print(
    {
        "first_event": min(times).isoformat() if times else None,
        "last_event": max(times).isoformat() if times else None,
    }
)
{'first_event': '2026-01-01T00:11:00+00:00', 'last_event': '2026-01-01T23:56:00+00:00'}

Record the generated shape and tutorial contract.#

summary = {
    "payments": len(data.behavior.payments),
    "events": len(data.behavior.payment_events),
}
print(summary)
assert summary["payments"] >= 0
{'payments': 100, 'events': 433}

Inspect stable payment identities.#

ids = [item.payment_id for item in data.behavior.payments]
assert len(ids) == len(set(ids))
print({"unique_payment_ids": len(ids)})
{'unique_payment_ids': 100}