Skip to content

Commit 39d93e0

Browse files
committed
feat: Add 8 platform default sagas as cookbook patterns
Publishes the default saga implementations from reference-data as discoverable cookbook patterns. Each pattern includes: - pattern.json with schema-compliant metadata (categories, complexity, provides/requires, registry dependencies) - manifest-fragment.yaml with account types and saga trigger declarations - The versioned .star file copied from services/reference-data/saga/defaults/ Patterns added: - default-deposit: current_account_deposit (complexity 3) - default-withdrawal: current_account_withdrawal (complexity 3) - default-payment-execution: payment_execution (complexity 5) - default-stripe-payment: stripe_payment v3.0.0 via financial gateway (complexity 8) - default-dividend-distribution: org-scoped syndicate distribution (complexity 5) - default-dunning-escalation: progressive dunning with account freeze (complexity 5) - default-dunning-unfreeze: account unfreeze and payment retry (complexity 5) - default-reconciliation-adjustment: immutable ledger correction (complexity 3) All scripts carry schema-validation: skip because they reference service modules (payment_order, correspondence) and builtins (build_org_account_ref) not yet registered in the Starlark handler schema, following the same convention as tote-betting and payg-energy patterns. Updates registry.json and allEconomyPatterns test array to include all 8.
1 parent 28e0e04 commit 39d93e0

26 files changed

Lines changed: 1421 additions & 0 deletions
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# schema-validation: skip
2+
# Saga: current_account_deposit
3+
# Version: 1.0.0
4+
# Previous: none
5+
# Changed: Updated field names: account_identification -> external_identifier, currency -> instrument_code
6+
# Author: Platform Team
7+
# Date: 2026-01-27
8+
#
9+
# This Starlark script defines the deposit saga workflow for the Current Account service.
10+
# The saga executes a multi-step deposit operation with compensation on failure.
11+
#
12+
# Steps (executed sequentially):
13+
# 1. log_position: Create CREDIT entry in PositionKeeping service
14+
# 2. initiate_booking_log: Create booking log in FinancialAccounting service
15+
# 3. capture_debit_posting: Post DEBIT to clearing account (double-entry source)
16+
# 4. capture_credit_posting: Post CREDIT to customer account
17+
# 5. finalize_booking_log: Transition booking log to POSTED
18+
# 6. save_account: Persist account metadata
19+
#
20+
# Compensation Order (LIFO - Last In, First Out):
21+
# Failures trigger compensation of completed steps in reverse order.
22+
# Compensation handlers are declared in handlers.yaml schema.
23+
#
24+
# Input data (provided via input_data dictionary):
25+
# - account_id: string - Account identifier
26+
# - external_identifier: string - External account identifier (e.g., IBAN)
27+
# - amount: string - Decimal amount as string (e.g., "100.50")
28+
# - instrument_code: string - Instrument code (e.g., "GBP", "kWh")
29+
# - transaction_id: string - Unique transaction identifier
30+
# - clearing_account_id: string - Clearing account for double-entry (optional)
31+
32+
# Define the deposit saga
33+
deposit_saga = saga(name="current_account_deposit")
34+
35+
# Define the saga execution function (required for conditional logic)
36+
def execute_deposit():
37+
# Extract input data
38+
account_id = input_data["account_id"]
39+
amount = Decimal(input_data["amount"])
40+
instrument_code = input_data["instrument_code"]
41+
transaction_id = input_data["transaction_id"]
42+
clearing_account_id = input_data.get("clearing_account_id", "")
43+
44+
# Step 1: Log position in PositionKeeping service with CREDIT direction
45+
step(name="log_position")
46+
log_position_result = position_keeping.initiate_log(
47+
position_id=account_id,
48+
amount=amount,
49+
instrument_code=instrument_code,
50+
direction="CREDIT",
51+
transaction_id=transaction_id,
52+
)
53+
54+
# Step 2: Initiate booking log in FinancialAccounting service
55+
step(name="initiate_booking_log")
56+
booking_log_result = financial_accounting.initiate_booking_log(
57+
account_id=account_id,
58+
instrument_code=instrument_code,
59+
transaction_id=transaction_id,
60+
transaction_type="DEPOSIT",
61+
)
62+
63+
# Step 3: Capture DEBIT posting to clearing account (if double-entry enabled)
64+
# For deposits: DEBIT the clearing account (where funds come from)
65+
if clearing_account_id != "":
66+
step(name="capture_debit_posting")
67+
debit_result = financial_accounting.capture_posting(
68+
booking_log_id=booking_log_result.booking_log_id,
69+
account_id=clearing_account_id,
70+
amount=amount,
71+
instrument_code=instrument_code,
72+
direction="DEBIT",
73+
transaction_id=transaction_id,
74+
posting_type="debit",
75+
)
76+
77+
# Step 4: Capture CREDIT posting to customer account
78+
step(name="capture_credit_posting")
79+
credit_result = financial_accounting.capture_posting(
80+
booking_log_id=booking_log_result.booking_log_id,
81+
account_id=account_id,
82+
amount=amount,
83+
instrument_code=instrument_code,
84+
direction="CREDIT",
85+
transaction_id=transaction_id,
86+
posting_type="credit",
87+
)
88+
89+
# Step 5: Finalize booking log (transition to POSTED)
90+
step(name="finalize_booking_log")
91+
finalize_result = financial_accounting.update_booking_log(
92+
booking_log_id=booking_log_result.booking_log_id,
93+
status="POSTED",
94+
)
95+
96+
# Step 6: Save account metadata
97+
step(name="save_account")
98+
save_result = current_account.save(
99+
account_id=account_id,
100+
transaction_id=transaction_id,
101+
)
102+
103+
# Output the saga result
104+
result = {
105+
"status": "COMPLETED",
106+
"transaction_id": transaction_id,
107+
"position_log_id": log_position_result.log_id,
108+
"booking_log_id": booking_log_result.booking_log_id,
109+
"credit_posting_id": credit_result.posting_id,
110+
}
111+
return result
112+
113+
# Execute the saga
114+
execute_deposit()
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
accountTypes:
2+
- code: CURRENT
3+
name: Current Account
4+
normalBalance: NORMAL_BALANCE_DEBIT
5+
allowedInstruments: [GBP]
6+
policies:
7+
validation: "amount > 0"
8+
bucketing: ""
9+
- code: CLEARING
10+
name: Clearing Account
11+
normalBalance: NORMAL_BALANCE_CREDIT
12+
allowedInstruments: [GBP]
13+
policies:
14+
validation: ""
15+
bucketing: ""
16+
17+
sagas:
18+
- name: current_account_deposit
19+
trigger: "api:/v1/accounts/{account_id}/deposit"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"$schema": "https://cookbook.meridianhub.org/schema/registry-item.json",
3+
"name": "default-deposit",
4+
"type": "registry:pattern",
5+
"title": "Current Account Deposit",
6+
"description": "Multi-step saga for depositing funds into a current account with double-entry ledger posting. Logs a CREDIT position in position keeping, initiates a booking log, posts DEBIT to clearing and CREDIT to customer account, then finalizes the booking to POSTED state.",
7+
"registryDependencies": ["base-fiat-gbp"],
8+
"categories": ["banking", "ledger", "double-entry"],
9+
"meta": {
10+
"complexity": 3,
11+
"design_pattern": "current-account-operation",
12+
"industries": ["banking", "fintech"],
13+
"provides": {
14+
"instruments": [],
15+
"account_types": ["CURRENT", "CLEARING"],
16+
"sagas": ["current_account_deposit"],
17+
"valuation_rules": [],
18+
"triggers": ["api:/v1/accounts/{account_id}/deposit"]
19+
},
20+
"requires": {
21+
"instruments": ["GBP"],
22+
"market_data": []
23+
},
24+
"composes_with": ["default-withdrawal"],
25+
"conflicts_with": [],
26+
"extends": ["base-fiat-gbp"]
27+
},
28+
"files": [
29+
{
30+
"path": "patterns/default-deposit/manifest-fragment.yaml",
31+
"type": "registry:file",
32+
"target": "~/manifest-fragment.yaml"
33+
},
34+
{
35+
"path": "patterns/default-deposit/current_account_deposit.star",
36+
"type": "registry:file",
37+
"target": "~/sagas/current_account_deposit.star"
38+
}
39+
]
40+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# schema-validation: skip
2+
# Saga: dividend_distribution
3+
# Version: 1.0.0
4+
# Previous: none
5+
# Changed: Initial implementation of org-scoped dividend distribution
6+
# Author: Platform Team
7+
# Date: 2026-02-16
8+
#
9+
# This Starlark script defines the dividend distribution saga for org-scoped
10+
# syndicate workflows. It demonstrates how to use party.list_participants,
11+
# party.get_structuring_data, build_org_account_ref, and resolve_account
12+
# with composite references to distribute funds across syndicate members.
13+
#
14+
# Steps (executed sequentially):
15+
# 1. list_participants: Retrieve all active syndicate participants
16+
# 2. For each participant:
17+
# a. get_structuring_data: Get allocation share metadata
18+
# b. resolve_account: Resolve participant's org-scoped account
19+
# c. log_position: Create CREDIT entry in PositionKeeping
20+
# 3. Return distribution result with per-participant details
21+
#
22+
# Input data (provided via input_data dictionary):
23+
# - org_id: string - Syndicate organization party ID
24+
# - total_amount: string - Total dividend amount as decimal string
25+
# - instrument_code: string - Instrument code (e.g., "GBP", "kWh")
26+
# - transaction_id: string - Unique transaction identifier
27+
28+
# Define the dividend distribution saga
29+
distribution_saga = saga(name="dividend_distribution")
30+
31+
def execute_distribution():
32+
# Extract input data
33+
org_id = input_data["org_id"]
34+
total_amount = Decimal(input_data["total_amount"])
35+
instrument_code = input_data["instrument_code"]
36+
transaction_id = input_data["transaction_id"]
37+
38+
# Step 1: List all active syndicate participants
39+
step(name="list_participants")
40+
participants = party.list_participants(
41+
org_id=org_id,
42+
relationship_type="RELATIONSHIP_TYPE_SYNDICATE_PARTICIPANT",
43+
)
44+
45+
distributions = []
46+
47+
# Step 2: For each participant, calculate and distribute
48+
for participant in participants:
49+
party_id = participant["party_id"]
50+
metadata = participant["metadata"]
51+
52+
# Get detailed structuring data if needed
53+
step(name="get_structuring_data_" + party_id)
54+
structuring = party.get_structuring_data(
55+
party_id=party_id,
56+
org_id=org_id,
57+
relationship_type="RELATIONSHIP_TYPE_SYNDICATE_PARTICIPANT",
58+
)
59+
60+
# Calculate participant's share
61+
allocation_share = Decimal(str(structuring.get("allocation_share", "0")))
62+
participant_amount = total_amount * allocation_share
63+
64+
# Build composite account reference for org-scoped resolution
65+
account_ref = build_org_account_ref(
66+
party_id=party_id,
67+
org_id=org_id,
68+
instrument_code=instrument_code,
69+
)
70+
71+
# Resolve the org-scoped account
72+
account_id = resolve_account(reference=account_ref)
73+
74+
# Log the credit position for this participant
75+
step(name="log_position_" + party_id)
76+
log_result = position_keeping.initiate_log(
77+
position_id=account_id,
78+
amount=participant_amount,
79+
instrument_code=instrument_code,
80+
direction="CREDIT",
81+
transaction_id=transaction_id,
82+
)
83+
84+
distributions.append({
85+
"party_id": party_id,
86+
"account_id": account_id,
87+
"amount": str(participant_amount),
88+
"log_id": log_result.log_id,
89+
})
90+
91+
# Output the saga result
92+
result = {
93+
"status": "COMPLETED",
94+
"transaction_id": transaction_id,
95+
"org_id": org_id,
96+
"participant_count": len(distributions),
97+
"distributions": distributions,
98+
}
99+
return result
100+
101+
# Execute the saga
102+
execute_distribution()
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Dividend distribution across org-scoped syndicate participants.
2+
# Uses party.list_participants and party.get_structuring_data to enumerate
3+
# members and their allocation shares, then resolves each participant's
4+
# org-scoped account via build_org_account_ref + resolve_account.
5+
# No new instruments or account types — operates on existing positions.
6+
sagas:
7+
- name: dividend_distribution
8+
trigger: "api:/v1/syndicates/{org_id}/distribute"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"$schema": "https://cookbook.meridianhub.org/schema/registry-item.json",
3+
"name": "default-dividend-distribution",
4+
"type": "registry:pattern",
5+
"title": "Dividend Distribution",
6+
"description": "Org-scoped syndicate dividend distribution saga. Lists all active participants, retrieves each participant's allocation share via structuring data, resolves their org-scoped account via composite account references, and credits each participant's position proportionally.",
7+
"registryDependencies": ["base-fiat-gbp"],
8+
"categories": ["distribution", "syndicate", "org-scoped"],
9+
"meta": {
10+
"complexity": 5,
11+
"design_pattern": "org-scoped-distribution",
12+
"industries": ["banking", "fintech", "betting"],
13+
"provides": {
14+
"instruments": [],
15+
"account_types": [],
16+
"sagas": ["dividend_distribution"],
17+
"valuation_rules": [],
18+
"triggers": ["api:/v1/syndicates/{org_id}/distribute"]
19+
},
20+
"requires": {
21+
"instruments": ["GBP"],
22+
"market_data": []
23+
},
24+
"composes_with": ["entity-distribution"],
25+
"conflicts_with": [],
26+
"extends": ["base-fiat-gbp"]
27+
},
28+
"files": [
29+
{
30+
"path": "patterns/default-dividend-distribution/manifest-fragment.yaml",
31+
"type": "registry:file",
32+
"target": "~/manifest-fragment.yaml"
33+
},
34+
{
35+
"path": "patterns/default-dividend-distribution/dividend_distribution.star",
36+
"type": "registry:file",
37+
"target": "~/sagas/dividend_distribution.star"
38+
}
39+
]
40+
}

0 commit comments

Comments
 (0)