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
| Account | Customer | Institution | Status | Balance |
|---|---|---|---|---|
| str | str | str | str | f64 |
| "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
| Payment | Rail | Amount | Type | Status |
|---|---|---|---|---|
| str | str | f64 | str | str |
| "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
| Event | Event time | Available at |
|---|---|---|
| str | datetime[μs, UTC] | datetime[μs, UTC] |
| "CARD_PAYMENT_INITIATED" | 2026-01-01 12:21:00 UTC | 2026-01-01 12:21:05 UTC |
| "CARD_AUTHORIZATION_REQUESTED" | 2026-01-01 12:21:01 UTC | 2026-01-01 12:21:06 UTC |
| "CARD_AUTHORIZED" | 2026-01-01 12:21:02 UTC | 2026-01-01 12:21:07 UTC |
| "CARD_CAPTURED" | 2026-01-01 12:21:07 UTC | 2026-01-01 12:21:12 UTC |
| "CARD_CLEARED" | 2026-01-01 12:21:37 UTC | 2026-01-01 12:21:42 UTC |
| "CARD_SETTLED" | 2026-01-01 12:22:37 UTC | 2026-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
| Entry | Payment | Account | Type | Amount | Balance after |
|---|---|---|---|---|---|
| str | str | str | str | f64 | f64 |
| "LED-EVT-00000017-01" | "PAY-00000017" | "ACC-000003" | "DEBIT" | 7.54 | 6299.57 |
| "LED-EVT-00000017-02" | "PAY-00000017" | "ACC-000011" | "CREDIT" | 7.54 | 13883.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}