Skip to content

Commit 8c4c5cc

Browse files
authored
refactor: rename accounts → pb_accounts (Phase 1 of normal accounts) (#26)
* docs: design for normal accounts (single-pool) alongside PB accounts Introduces normal accounts as a parallel domain backed by a single TigerBeetle account. Trust money enters via a normal account and reaches PB accounts only through internal transfers; the direct trust→PB deposit path is removed. Covers schema (rename accounts→pb_accounts, new normal_accounts, transactions discriminator + correlation_id), API surface (/pb-accounts and /normal-accounts canonical, /accounts as in-process alias with Sunset headers), service & ledger conventions, transfer flow with shared correlation_id and tb_transfer_id across two journal rows, and a three-PR rollout plan. Bite-sized, TDD-driven plan covering all three phases: rename accounts→pb_accounts (PR 1), add normal accounts (PR 2), and add normal→PB transfers + remove direct trust deposit (PR 3).
1 parent c453499 commit 8c4c5cc

13 files changed

Lines changed: 4866 additions & 91 deletions

crates/pba_service/src/admin/handlers.rs

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,15 @@ struct DashboardTemplate {
4848
}
4949

5050
pub async fn dashboard(State(state): State<AppState>) -> Response {
51-
let status_counts = match state.account_repo.count_accounts_by_status().await {
51+
let status_counts = match state.pb_account_repo.count_accounts_by_status().await {
5252
Ok(c) => c,
5353
Err(e) => {
5454
tracing::error!("Failed to count accounts: {e}");
5555
return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response();
5656
}
5757
};
5858

59-
let purpose_counts = match state.account_repo.count_accounts_by_purpose().await {
59+
let purpose_counts = match state.pb_account_repo.count_accounts_by_purpose().await {
6060
Ok(c) => c,
6161
Err(e) => {
6262
tracing::error!("Failed to count by purpose: {e}");
@@ -114,15 +114,15 @@ async fn render_accounts_list(
114114
error: Option<String>,
115115
success: Option<String>,
116116
) -> Response {
117-
let accounts = match state.account_repo.list_accounts().await {
117+
let accounts = match state.pb_account_repo.list_accounts().await {
118118
Ok(a) => a,
119119
Err(e) => {
120120
tracing::error!("Failed to list accounts: {e}");
121121
return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response();
122122
}
123123
};
124124

125-
let purpose_codes = match state.account_repo.list_purpose_types().await {
125+
let purpose_codes = match state.pb_account_repo.list_purpose_types().await {
126126
Ok(pts) => pts.into_iter().map(|p| p.purpose_code).collect(),
127127
Err(_) => vec![],
128128
};
@@ -193,7 +193,7 @@ pub async fn create_account(
193193
};
194194

195195
match state
196-
.account_service
196+
.pb_account_service
197197
.create_account(
198198
holder_id,
199199
&form.purpose_code,
@@ -229,7 +229,7 @@ pub async fn update_account_status(
229229
};
230230

231231
match state
232-
.account_service
232+
.pb_account_service
233233
.update_status(account_id, status)
234234
.await
235235
{
@@ -278,7 +278,7 @@ pub async fn account_detail(
278278
State(state): State<AppState>,
279279
Path(account_id): Path<Uuid>,
280280
) -> Response {
281-
let account = match state.account_repo.get_account(account_id).await {
281+
let account = match state.pb_account_repo.get_account(account_id).await {
282282
Ok(a) => a,
283283
Err(e) => {
284284
tracing::error!("Account not found: {e}");
@@ -463,7 +463,7 @@ struct DepositTemplate {
463463
}
464464

465465
pub async fn deposit_form(State(state): State<AppState>, Path(account_id): Path<Uuid>) -> Response {
466-
let account = match state.account_repo.get_account(account_id).await {
466+
let account = match state.pb_account_repo.get_account(account_id).await {
467467
Ok(a) => a,
468468
Err(_) => return (StatusCode::NOT_FOUND, "Account not found").into_response(),
469469
};
@@ -495,7 +495,7 @@ pub async fn process_deposit(
495495
let gateway_ref = form.gateway_ref.as_deref().filter(|s| !s.is_empty());
496496
let funding_type = form.funding_type.as_deref().filter(|s| !s.is_empty());
497497
match state
498-
.deposit_service
498+
.pb_deposit_service
499499
.deposit(
500500
account_id,
501501
&form.source_ifsc,
@@ -513,7 +513,7 @@ pub async fn process_deposit(
513513
.into_response(),
514514
Err(e) => {
515515
let purpose_code = state
516-
.account_repo
516+
.pb_account_repo
517517
.get_account(account_id)
518518
.await
519519
.map(|a| a.purpose_code)
@@ -533,7 +533,7 @@ pub async fn post_deposit(
533533
Path((account_id, deposit_id)): Path<(Uuid, Uuid)>,
534534
) -> Response {
535535
match state
536-
.deposit_service
536+
.pb_deposit_service
537537
.post_deposit(account_id, deposit_id)
538538
.await
539539
{
@@ -552,7 +552,7 @@ pub async fn void_deposit(
552552
Path((account_id, deposit_id)): Path<(Uuid, Uuid)>,
553553
) -> Response {
554554
match state
555-
.deposit_service
555+
.pb_deposit_service
556556
.void_deposit(account_id, deposit_id, None)
557557
.await
558558
{
@@ -576,7 +576,7 @@ struct PaymentTemplate {
576576
}
577577

578578
pub async fn payment_form(State(state): State<AppState>, Path(account_id): Path<Uuid>) -> Response {
579-
let account = match state.account_repo.get_account(account_id).await {
579+
let account = match state.pb_account_repo.get_account(account_id).await {
580580
Ok(a) => a,
581581
Err(_) => return (StatusCode::NOT_FOUND, "Account not found").into_response(),
582582
};
@@ -604,7 +604,7 @@ pub async fn process_payment(
604604
) -> Response {
605605
let gateway_ref = form.gateway_ref.as_deref().filter(|s| !s.is_empty());
606606
match state
607-
.payment_service
607+
.pb_payment_service
608608
.make_payment(
609609
account_id,
610610
form.amount,
@@ -620,7 +620,7 @@ pub async fn process_payment(
620620
.into_response(),
621621
Err(e) => {
622622
let purpose_code = state
623-
.account_repo
623+
.pb_account_repo
624624
.get_account(account_id)
625625
.await
626626
.map(|a| a.purpose_code)
@@ -648,7 +648,7 @@ pub async fn withdrawal_form(
648648
State(state): State<AppState>,
649649
Path(account_id): Path<Uuid>,
650650
) -> Response {
651-
let account = match state.account_repo.get_account(account_id).await {
651+
let account = match state.pb_account_repo.get_account(account_id).await {
652652
Ok(a) => a,
653653
Err(_) => return (StatusCode::NOT_FOUND, "Account not found").into_response(),
654654
};
@@ -673,15 +673,15 @@ pub async fn process_withdrawal(
673673
) -> Response {
674674
let gateway_ref = form.gateway_ref.as_deref().filter(|s| !s.is_empty());
675675
match state
676-
.withdrawal_service
676+
.pb_withdrawal_service
677677
.withdraw(account_id, form.amount, None, gateway_ref)
678678
.await
679679
{
680680
Ok(_) => Redirect::to(&prefixed(&state, &format!("/admin/accounts/{account_id}")))
681681
.into_response(),
682682
Err(e) => {
683683
let purpose_code = state
684-
.account_repo
684+
.pb_account_repo
685685
.get_account(account_id)
686686
.await
687687
.map(|a| a.purpose_code)
@@ -929,7 +929,7 @@ struct PurposeTypesTemplate {
929929
}
930930

931931
pub async fn purpose_types_page(State(state): State<AppState>) -> Response {
932-
let purpose_types = match state.account_repo.list_purpose_types().await {
932+
let purpose_types = match state.pb_account_repo.list_purpose_types().await {
933933
Ok(pts) => pts,
934934
Err(e) => {
935935
tracing::error!("Failed to list purpose types: {e}");
@@ -993,7 +993,7 @@ pub async fn transaction_detail(
993993

994994
let dash = "—".to_string();
995995

996-
let (holder_id, purpose_code) = match state.account_repo.get_account(txn.account_id).await {
996+
let (holder_id, purpose_code) = match state.pb_account_repo.get_account(txn.account_id).await {
997997
Ok(a) => (a.holder_id, a.purpose_code),
998998
Err(e) => {
999999
tracing::warn!("Failed to load parent account for transaction {transaction_id}: {e}");
@@ -1101,7 +1101,7 @@ pub async fn post_transaction(
11011101
Err(_) => return (StatusCode::NOT_FOUND, "Transaction not found").into_response(),
11021102
};
11031103
if let Err(e) = state
1104-
.deposit_service
1104+
.pb_deposit_service
11051105
.post_deposit(txn.account_id, transaction_id)
11061106
.await
11071107
{
@@ -1123,7 +1123,7 @@ pub async fn void_transaction(
11231123
Err(_) => return (StatusCode::NOT_FOUND, "Transaction not found").into_response(),
11241124
};
11251125
if let Err(e) = state
1126-
.deposit_service
1126+
.pb_deposit_service
11271127
.void_deposit(txn.account_id, transaction_id, None)
11281128
.await
11291129
{

crates/pba_service/src/api/handlers.rs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub async fn create_account(
1818
let origin_account_number = AccountNumber::parse(&req.origin_account_number)?;
1919

2020
let account = state
21-
.account_service
21+
.pb_account_service
2222
.create_account(
2323
&req.holder_id,
2424
&req.purpose_code,
@@ -34,7 +34,7 @@ pub async fn get_account(
3434
State(state): State<AppState>,
3535
Path(account_id): Path<Uuid>,
3636
) -> Result<Json<AccountResponse>, AppError> {
37-
let account = state.account_service.get_account(account_id).await?;
37+
let account = state.pb_account_service.get_account(account_id).await?;
3838
Ok(Json(account.into()))
3939
}
4040

@@ -46,7 +46,7 @@ pub async fn update_account_status(
4646
let status = AccountStatus::from_str(&req.status)
4747
.ok_or_else(|| AppError::DatabaseError(format!("Invalid status: {}", req.status)))?;
4848
let account = state
49-
.account_service
49+
.pb_account_service
5050
.update_status(account_id, status)
5151
.await?;
5252
Ok(Json(account.into()))
@@ -58,7 +58,7 @@ pub async fn get_balance(
5858
State(state): State<AppState>,
5959
Path(account_id): Path<Uuid>,
6060
) -> Result<Json<BalanceResponse>, AppError> {
61-
let account = state.account_service.get_account(account_id).await?;
61+
let account = state.pb_account_service.get_account(account_id).await?;
6262
let balance = state
6363
.ledger_repo
6464
.get_balance(account.tb_self_account_id, account.tb_others_account_id)
@@ -82,7 +82,7 @@ pub async fn deposit(
8282
Json(req): Json<DepositRequest>,
8383
) -> Result<(axum::http::StatusCode, Json<DepositResponse>), AppError> {
8484
let result = state
85-
.deposit_service
85+
.pb_deposit_service
8686
.deposit(
8787
account_id,
8888
&req.source_ifsc,
@@ -116,7 +116,7 @@ pub async fn post_deposit(
116116
Path((account_id, deposit_id)): Path<(Uuid, Uuid)>,
117117
) -> Result<Json<DepositResponse>, AppError> {
118118
let result = state
119-
.deposit_service
119+
.pb_deposit_service
120120
.post_deposit(account_id, deposit_id)
121121
.await?;
122122

@@ -138,7 +138,7 @@ pub async fn void_deposit(
138138
Json(req): Json<VoidDepositRequest>,
139139
) -> Result<Json<DepositResponse>, AppError> {
140140
let result = state
141-
.deposit_service
141+
.pb_deposit_service
142142
.void_deposit(account_id, deposit_id, req.reason.as_deref())
143143
.await?;
144144

@@ -162,7 +162,7 @@ pub async fn make_payment(
162162
Json(req): Json<PaymentRequest>,
163163
) -> Result<(axum::http::StatusCode, Json<PaymentResponse>), AppError> {
164164
let result = state
165-
.payment_service
165+
.pb_payment_service
166166
.make_payment(
167167
account_id,
168168
req.amount,
@@ -196,7 +196,7 @@ pub async fn withdraw(
196196
Json(req): Json<WithdrawalRequest>,
197197
) -> Result<(axum::http::StatusCode, Json<WithdrawalResponse>), AppError> {
198198
let result = state
199-
.withdrawal_service
199+
.pb_withdrawal_service
200200
.withdraw(
201201
account_id,
202202
req.amount,
@@ -223,7 +223,7 @@ pub async fn list_transactions(
223223
axum::extract::Query(query): axum::extract::Query<ListTransactionsQuery>,
224224
) -> Result<Json<ListTransactionsResponse>, AppError> {
225225
// Verify account exists
226-
let _ = state.account_service.get_account(account_id).await?;
226+
let _ = state.pb_account_service.get_account(account_id).await?;
227227

228228
let offset = query.offset.unwrap_or(0).max(0);
229229
let limit = query.limit.unwrap_or(20).clamp(1, 100);
@@ -274,7 +274,7 @@ pub async fn list_all_transactions(
274274
pub async fn list_purpose_types(
275275
State(state): State<AppState>,
276276
) -> Result<Json<ListPurposeTypesResponse>, AppError> {
277-
let types = state.account_repo.list_purpose_types().await?;
277+
let types = state.pb_account_repo.list_purpose_types().await?;
278278
Ok(Json(ListPurposeTypesResponse {
279279
purpose_types: types.into_iter().map(|t| t.into()).collect(),
280280
}))
@@ -284,7 +284,10 @@ pub async fn get_purpose_type(
284284
State(state): State<AppState>,
285285
Path(purpose_code): Path<String>,
286286
) -> Result<Json<PurposeTypeResponse>, AppError> {
287-
let purpose = state.account_repo.get_purpose_type(&purpose_code).await?;
287+
let purpose = state
288+
.pb_account_repo
289+
.get_purpose_type(&purpose_code)
290+
.await?;
288291
Ok(Json(purpose.into()))
289292
}
290293

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- Rename the dual-pool account table to pb_accounts (purpose-bound).
2+
-- A future migration introduces normal_accounts as a sibling table.
3+
ALTER TABLE accounts RENAME TO pb_accounts;
4+
-- NOTE: idx_accounts_origin_purpose was already dropped in
5+
-- 20260428000001_drop_origin_purpose_uniqueness.sql; no rename needed here.
6+
ALTER INDEX idx_accounts_holder RENAME TO idx_pb_accounts_holder;
7+
8+
-- The existing FK on transactions.account_id was created as
9+
-- transactions_account_id_fkey REFERENCES accounts(id). Postgres preserves
10+
-- the FK target across the rename automatically. We do NOT drop it here —
11+
-- the FK now points at pb_accounts(id), which is correct for Phase 1.

0 commit comments

Comments
 (0)