Skip to content

Commit 5803c68

Browse files
committed
feat: load customer wallets automatically
1 parent 320f6b4 commit 5803c68

11 files changed

Lines changed: 289 additions & 29 deletions

File tree

backend/src/main/java/com/payledger/platform/shared/api/OpenApiController.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ public Map<String, Object> apiDocs() {
2929
private Map<String, Object> paths() {
3030
return Map.ofEntries(
3131
Map.entry("/api/v1/wallets", Map.of(
32-
"post", operation(
33-
"Create wallet",
34-
"Create an authenticated customer's wallet."
32+
"get", operation(
33+
"List my wallets",
34+
"Returns wallets owned by the authenticated customer."
3535
)
3636
)),
3737
Map.entry("/api/v1/wallets/{walletId}/balance", Map.of(

backend/src/main/java/com/payledger/platform/wallet/api/WalletController.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.payledger.platform.wallet.api;
22

3+
import com.payledger.platform.identity.application.AuthenticatedCustomer;
4+
import com.payledger.platform.identity.application.CurrentCustomerService;
35
import com.payledger.platform.wallet.application.WalletService;
46
import jakarta.validation.Valid;
57
import org.springframework.http.HttpStatus;
@@ -11,16 +13,22 @@
1113
import org.springframework.web.bind.annotation.RequestMapping;
1214
import org.springframework.web.bind.annotation.RestController;
1315

16+
import java.util.List;
1417
import java.util.UUID;
1518

1619
@RestController
1720
@RequestMapping("/api/v1")
1821
public class WalletController {
1922

2023
private final WalletService walletService;
24+
private final CurrentCustomerService currentCustomerService;
2125

22-
public WalletController(WalletService walletService) {
26+
public WalletController(
27+
WalletService walletService,
28+
CurrentCustomerService currentCustomerService
29+
) {
2330
this.walletService = walletService;
31+
this.currentCustomerService = currentCustomerService;
2432
}
2533

2634
@PostMapping("/customers/{customerId}/wallets")
@@ -39,4 +47,16 @@ public ResponseEntity<WalletResponse> createWallet(
3947
public WalletResponse getWallet(@PathVariable UUID walletId) {
4048
return WalletResponse.from(walletService.getWallet(walletId));
4149
}
50+
51+
@GetMapping("/wallets")
52+
public List<WalletResponse> listMyWallets() {
53+
AuthenticatedCustomer currentCustomer =
54+
currentCustomerService.getCurrentCustomer();
55+
56+
return walletService
57+
.listWalletsForCustomer(currentCustomer.customerId())
58+
.stream()
59+
.map(WalletResponse::from)
60+
.toList();
61+
}
4262
}

backend/src/main/java/com/payledger/platform/wallet/application/WalletService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.springframework.transaction.annotation.Transactional;
1515

1616
import java.util.Currency;
17+
import java.util.List;
1718
import java.util.Locale;
1819
import java.util.Map;
1920
import java.util.UUID;
@@ -101,6 +102,11 @@ public Wallet getWallet(UUID walletId) {
101102
);
102103
}
103104

105+
@Transactional(readOnly = true)
106+
public List<Wallet> listWalletsForCustomer(UUID customerId) {
107+
return walletRepository.findByCustomerIdOrderByCurrencyAsc(customerId);
108+
}
109+
104110
private String normalizeCurrency(String requestedCurrency) {
105111
String currency = requestedCurrency.trim().toUpperCase(Locale.ROOT);
106112

backend/src/main/java/com/payledger/platform/wallet/infrastructure/WalletRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
import com.payledger.platform.wallet.domain.Wallet;
44
import org.springframework.data.jpa.repository.JpaRepository;
55

6+
import java.util.List;
67
import java.util.Optional;
78
import java.util.UUID;
89

910
public interface WalletRepository extends JpaRepository<Wallet, UUID> {
1011

1112
Optional<Wallet> findByCustomerIdAndCurrency(UUID customerId, String currency);
13+
14+
List<Wallet> findByCustomerIdOrderByCurrencyAsc(UUID customerId);
1215
}

backend/src/test/java/com/payledger/platform/wallet/api/WalletBalanceApiIntegrationTest.java

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,37 @@ void returnsAvailableBalanceAfterActiveHolds() throws Exception {
120120
.andExpect(jsonPath("$.availableBalanceMinor").value(10_000));
121121
}
122122

123+
@Test
124+
void listsOnlyWalletsOwnedByLinkedCustomer() throws Exception {
125+
WalletContext tryWallet = createWallet("list-owned-try", "TRY");
126+
Wallet usdWallet = walletService.createWallet(
127+
tryWallet.customer().getId(),
128+
"USD"
129+
);
130+
createTryWallet("list-other");
131+
132+
String subject = "wallet-list-subject-" + UUID.randomUUID();
133+
customerIdentityService.linkKeycloakIdentity(
134+
tryWallet.customer().getId(),
135+
subject
136+
);
137+
138+
mockMvc.perform(
139+
get("/api/v1/wallets")
140+
.with(com.payledger.platform.shared.security.TestJwtSupport.customerJwt(subject))
141+
)
142+
.andExpect(status().isOk())
143+
.andExpect(jsonPath("$.length()").value(2))
144+
.andExpect(jsonPath("$[0].customerId")
145+
.value(tryWallet.customer().getId().toString()))
146+
.andExpect(jsonPath("$[1].customerId")
147+
.value(tryWallet.customer().getId().toString()))
148+
.andExpect(jsonPath("$[?(@.id == '%s')]",
149+
tryWallet.wallet().getId().toString()).exists())
150+
.andExpect(jsonPath("$[?(@.id == '%s')]",
151+
usdWallet.getId().toString()).exists());
152+
}
153+
123154
@Test
124155
void returns403WhenLinkedCustomerReadsAnotherCustomersBalance()
125156
throws Exception {
@@ -155,7 +186,23 @@ void returns403WhenJwtSubjectIsNotLinked() throws Exception {
155186
.andExpect(jsonPath("$.code").value("IDENTITY_NOT_LINKED"));
156187
}
157188

189+
@Test
190+
void listWalletsReturns403WhenJwtSubjectIsNotLinked() throws Exception {
191+
mockMvc.perform(
192+
get("/api/v1/wallets")
193+
.with(com.payledger.platform.shared.security.TestJwtSupport.customerJwt(
194+
"unlinked-wallet-list-subject-" + UUID.randomUUID()
195+
))
196+
)
197+
.andExpect(status().isForbidden())
198+
.andExpect(jsonPath("$.code").value("IDENTITY_NOT_LINKED"));
199+
}
200+
158201
private WalletContext createTryWallet(String label) {
202+
return createWallet(label, "TRY");
203+
}
204+
205+
private WalletContext createWallet(String label, String currency) {
159206
String suffix = UUID.randomUUID()
160207
.toString()
161208
.replace("-", "")
@@ -168,7 +215,7 @@ private WalletContext createTryWallet(String label) {
168215
label + "-" + suffix + "@example.test"
169216
);
170217

171-
Wallet wallet = walletService.createWallet(customer.getId(), "TRY");
218+
Wallet wallet = walletService.createWallet(customer.getId(), currency);
172219

173220
LedgerAccount ledgerAccount = ledgerAccountRepository
174221
.findByWalletId(wallet.getId())

frontend/src/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
SettlementBatch,
1515
TransferRequest,
1616
TransferResponse,
17+
Wallet,
1718
WalletBalance,
1819
WalletStatement
1920
} from "./types";
@@ -23,6 +24,10 @@ type TokenProvider = () => Promise<string | null>;
2324
export class ApiClient {
2425
constructor(private readonly tokenProvider: TokenProvider) {}
2526

27+
myWallets(): Promise<Wallet[]> {
28+
return this.request("/api/v1/wallets");
29+
}
30+
2631
walletBalance(walletId: string): Promise<WalletBalance> {
2732
return this.request(`/api/v1/wallets/${walletId}/balance`);
2833
}

0 commit comments

Comments
 (0)