Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public class FinancialDataService {

private HttpClient httpClient;

// Cached Plaid sandbox access token (acquired via public_token create+exchange).
private volatile String plaidAccessToken;

@PostConstruct
void init() {
this.httpClient = HttpClient.newBuilder()
Expand All @@ -60,22 +63,92 @@ public void enrichAsync(String accountNumber) {
}

private CompletableFuture<Void> fetchPlaidBalance(String accountNumber) {
String body = "{\"access_token\":\"access-sandbox-" + accountNumber + "\"}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://sandbox.plaid.com/accounts/balance/get"))
return ensurePlaidAccessToken().thenCompose(token -> {
if (token == null) {
logger.warn("Plaid access token unavailable; skipping balance call");
return CompletableFuture.completedFuture(null);
}
String body = "{\"access_token\":\"" + token + "\"}";
HttpRequest request = plaidRequest("/accounts/balance/get", body);
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenAccept(resp -> logger.info("Plaid balance response: status={}", resp.statusCode()))
.exceptionally(ex -> {
logger.warn("Plaid balance call failed: {}", ex.getMessage());
return null;
});
});
}

// Plaid sandbox tokens are not constructible: create a public_token then exchange it
// for an access_token. Cached and reused across accounts.
private CompletableFuture<String> ensurePlaidAccessToken() {
String cached = plaidAccessToken;
if (cached != null) {
return CompletableFuture.completedFuture(cached);
}
String createBody = "{\"institution_id\":\"ins_109508\",\"initial_products\":[\"transactions\"]}";
HttpRequest createReq = plaidRequest("/sandbox/public_token/create", createBody);
return httpClient.sendAsync(createReq, HttpResponse.BodyHandlers.ofString())
.thenCompose(createResp -> {
String publicToken = extractJsonString(createResp.body(), "public_token");
if (publicToken == null) {
logger.warn("Plaid public_token create failed: status={}", createResp.statusCode());
return CompletableFuture.completedFuture((String) null);
}
HttpRequest exchangeReq = plaidRequest("/item/public_token/exchange",
"{\"public_token\":\"" + publicToken + "\"}");
return httpClient.sendAsync(exchangeReq, HttpResponse.BodyHandlers.ofString())
.thenApply(exchangeResp -> {
String token = extractJsonString(exchangeResp.body(), "access_token");
if (token != null) {
plaidAccessToken = token;
logger.info("Plaid sandbox access token acquired");
} else {
logger.warn("Plaid token exchange failed: status={}", exchangeResp.statusCode());
}
return token;
});
})
.exceptionally(ex -> {
logger.warn("Plaid token setup failed: {}", ex.getMessage());
return null;
});
}

private HttpRequest plaidRequest(String path, String body) {
return HttpRequest.newBuilder()
.uri(URI.create("https://sandbox.plaid.com" + path))
.timeout(TIMEOUT)
.header("Content-Type", "application/json")
.header("PLAID-CLIENT-ID", plaidClientId)
.header("PLAID-SECRET", plaidSecret)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
}

return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenAccept(resp -> logger.info("Plaid balance response: status={}", resp.statusCode()))
.exceptionally(ex -> {
logger.warn("Plaid balance call failed: {}", ex.getMessage());
return null;
});
// Minimal "key":"value" string extractor — avoids pulling in a JSON dependency here.
private static String extractJsonString(String json, String key) {
if (json == null) {
return null;
}
String needle = "\"" + key + "\"";
int k = json.indexOf(needle);
if (k < 0) {
return null;
}
int colon = json.indexOf(':', k + needle.length());
if (colon < 0) {
return null;
}
int q1 = json.indexOf('"', colon + 1);
if (q1 < 0) {
return null;
}
int q2 = json.indexOf('"', q1 + 1);
if (q2 < 0) {
return null;
}
return json.substring(q1 + 1, q2);
}

private CompletableFuture<Void> fetchExchangeRates() {
Expand Down
6 changes: 0 additions & 6 deletions kubernetes/base/deployments/accounts-service-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,8 @@ spec:
key: secret
- name: JAVA_OPTS
value: "-Xms128m -Xmx384m -XX:MaxMetaspaceSize=128m -XX:ReservedCodeCacheSize=32m -XX:MaxDirectMemorySize=16m -XX:+UseSerialGC -XX:+UseStringDeduplication -XX:+UseContainerSupport -Djava.security.egd=file:/dev/./urandom -Dspring.main.lazy-initialization=true"
- name: AWS_S3_BUCKET
value: "banking-app-statements"
- name: AWS_REGION
value: "us-east-1"
- name: AWS_ACCESS_KEY_ID
value: "AKIAIOSFODNN7EXAMPLE"
- name: AWS_SECRET_ACCESS_KEY
value: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
envFrom:
- configMapRef:
name: app-config
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: banking-accounts
namespace: banking-app
spec:
template:
spec:
containers:
- name: accounts-service
imagePullPolicy: IfNotPresent
env:
- name: PLAID_CLIENT_ID
valueFrom:
secretKeyRef:
name: banking-thirdparty-keys
key: PLAID_CLIENT_ID
- name: PLAID_SECRET
valueFrom:
secretKeyRef:
name: banking-thirdparty-keys
key: PLAID_SECRET
- name: EXCHANGE_RATES_APP_ID
valueFrom:
secretKeyRef:
name: banking-thirdparty-keys
key: EXCHANGE_RATES_APP_ID
- name: MOODYS_API_KEY
valueFrom:
secretKeyRef:
name: banking-thirdparty-keys
key: MOODYS_API_KEY
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: banking-thirdparty-keys
key: AWS_ACCESS_KEY_ID
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: banking-thirdparty-keys
key: AWS_SECRET_ACCESS_KEY
- name: AWS_S3_BUCKET
value: "speedscale-banking-demo-statements"
36 changes: 36 additions & 0 deletions kubernetes/overlays/speedscale/ai-service-thirdparty-env.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: banking-ai
namespace: banking-app
spec:
template:
spec:
containers:
- name: ai-service
env:
- name: AI_API_KEY
valueFrom:
secretKeyRef:
name: banking-thirdparty-keys
key: AI_API_KEY
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: banking-thirdparty-keys
key: OPENAI_API_KEY
- name: GEMINI_API_KEY
valueFrom:
secretKeyRef:
name: banking-thirdparty-keys
key: GEMINI_API_KEY
- name: XAI_API_KEY
valueFrom:
secretKeyRef:
name: banking-thirdparty-keys
key: XAI_API_KEY
- name: OPENROUTER_API_KEY
valueFrom:
secretKeyRef:
name: banking-thirdparty-keys
key: OPENROUTER_API_KEY
Loading
Loading