Skip to content

Commit 198b6d8

Browse files
committed
Implement balance alerts feature
1 parent 7e20af6 commit 198b6d8

9 files changed

Lines changed: 273 additions & 16 deletions

File tree

bear-ir/account.bear.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ block:
6868
- port: accountStore
6969
kind: external
7070
ops: [get, put]
71+
- port: alertLog
72+
kind: block
73+
targetOps: [AppendAlert]
7174
- port: transactionLog
7275
kind: block
7376
targetOps: [AppendTransaction]
@@ -107,6 +110,10 @@ block:
107110
kind: block
108111
targetBlock: transaction-log
109112
targetOps: [AppendTransaction]
113+
- port: alertLog
114+
kind: block
115+
targetBlock: alert-log
116+
targetOps: [AppendAlert]
110117
- port: idempotency
111118
kind: external
112119
ops: [get, put]
@@ -119,4 +126,4 @@ block:
119126
- kind: non_negative
120127
field: balanceCents
121128
- kind: non_negative
122-
field: txSeq
129+
field: txSeq

bear-ir/alert-log.bear.yaml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
version: v1
2+
block:
3+
name: AlertLog
4+
kind: logic
5+
operations:
6+
- name: AppendAlert
7+
contract:
8+
inputs:
9+
- name: accountId
10+
type: string
11+
- name: alertType
12+
type: string
13+
- name: requestId
14+
type: string
15+
- name: message
16+
type: string
17+
outputs:
18+
- name: alertSeq
19+
type: int
20+
uses:
21+
allow:
22+
- port: alertStore
23+
kind: external
24+
ops: [append]
25+
invariants:
26+
- kind: non_negative
27+
field: alertSeq
28+
- name: GetAlerts
29+
contract:
30+
inputs:
31+
- name: accountId
32+
type: string
33+
outputs:
34+
- name: alertsJson
35+
type: string
36+
uses:
37+
allow:
38+
- port: alertStore
39+
kind: external
40+
ops: [list]
41+
effects:
42+
allow:
43+
- port: alertStore
44+
kind: external
45+
ops: [append, list]
46+
invariants:
47+
- kind: non_negative
48+
field: alertSeq

bear.blocks.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,8 @@ blocks:
77
- name: transaction-log
88
ir: bear-ir/transaction-log.bear.yaml
99
projectRoot: .
10-
enabled: true
10+
enabled: true
11+
- name: alert-log
12+
ir: bear-ir/alert-log.bear.yaml
13+
projectRoot: .
14+
enabled: true

src/main/java/blocks/account/impl/AccountImpl.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@
1212
import com.bear.generated.account.Account_GetBalanceResult;
1313
import com.bear.generated.account.Account_WithdrawRequest;
1414
import com.bear.generated.account.Account_WithdrawResult;
15+
import com.bear.generated.account.AlertLogPort;
1516
import com.bear.generated.account.BearValue;
1617
import com.bear.generated.account.TransactionLogPort;
1718

1819
public final class AccountImpl implements AccountLogic {
20+
private static final String INSUFFICIENT_FUNDS_ALERT = "INSUFFICIENT_FUNDS_ATTEMPT";
21+
1922
@Override
2023
public Account_CreateAccountResult executeCreateAccount(Account_CreateAccountRequest request, AccountStorePort accountStorePort) {
2124
requireNonBlank(request.getOwnerId(), "ownerId");
@@ -48,11 +51,21 @@ public Account_GetBalanceResult executeGetBalance(Account_GetBalanceRequest requ
4851
}
4952

5053
@Override
51-
public Account_WithdrawResult executeWithdraw(Account_WithdrawRequest request, AccountStorePort accountStorePort, TransactionLogPort transactionLogPort) {
54+
public Account_WithdrawResult executeWithdraw(
55+
Account_WithdrawRequest request,
56+
AccountStorePort accountStorePort,
57+
AlertLogPort alertLogPort,
58+
TransactionLogPort transactionLogPort
59+
) {
5260
int amountCents = requirePositiveAmount(request.getAmountCents());
5361
requireNonBlank(request.getRequestId(), "requestId");
5462
AccountState account = loadAccount(request.getAccountId(), accountStorePort);
5563
if (account.balanceCents() < amountCents) {
64+
appendAlert(
65+
alertLogPort,
66+
account.accountId(),
67+
request.getRequestId(),
68+
"Insufficient funds for withdrawal attempt of " + amountCents + " cents");
5669
throw new InsufficientFundsException(account.accountId());
5770
}
5871

@@ -82,6 +95,16 @@ private static BearValue toAccountValue(String accountId, String ownerId, int ba
8295
.build();
8396
}
8497

98+
private static void appendAlert(AlertLogPort alertLogPort, String accountId, String requestId, String message) {
99+
alertLogPort.call(BearValue.builder()
100+
.put("op", "AppendAlert")
101+
.put("accountId", accountId)
102+
.put("alertType", INSUFFICIENT_FUNDS_ALERT)
103+
.put("requestId", requestId)
104+
.put("message", message)
105+
.build());
106+
}
107+
85108
private static int appendTransaction(
86109
TransactionLogPort transactionLogPort,
87110
String accountId,
@@ -123,4 +146,4 @@ private static int parseInt(String raw, String field) {
123146

124147
private record AccountState(String accountId, String ownerId, int balanceCents) {
125148
}
126-
}
149+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package blocks.alert.log.adapter;
2+
3+
import java.util.ArrayList;
4+
import java.util.LinkedHashMap;
5+
import java.util.List;
6+
import java.util.Map;
7+
8+
import com.bear.generated.alert.log.AlertStorePort;
9+
import com.bear.generated.alert.log.BearValue;
10+
11+
public final class InMemoryAlertStorePort implements AlertStorePort {
12+
private final Map<String, List<AlertRecord>> alertsByAccount = new LinkedHashMap<>();
13+
14+
@Override
15+
public BearValue append(BearValue input) {
16+
String accountId = required(input, "accountId");
17+
List<AlertRecord> alerts = alertsByAccount.computeIfAbsent(accountId, ignored -> new ArrayList<>());
18+
int nextSeq = alerts.size() + 1;
19+
alerts.add(new AlertRecord(
20+
nextSeq,
21+
accountId,
22+
required(input, "alertType"),
23+
required(input, "requestId"),
24+
required(input, "message")));
25+
return BearValue.builder().put("alertSeq", Integer.toString(nextSeq)).build();
26+
}
27+
28+
@Override
29+
public BearValue list(BearValue input) {
30+
String accountId = required(input, "accountId");
31+
List<AlertRecord> alerts = alertsByAccount.getOrDefault(accountId, List.of());
32+
StringBuilder json = new StringBuilder("[");
33+
boolean first = true;
34+
for (AlertRecord alert : alerts) {
35+
if (!first) {
36+
json.append(',');
37+
}
38+
first = false;
39+
json.append('{')
40+
.append("\"alertSeq\":").append(alert.alertSeq())
41+
.append(",\"accountId\":\"").append(escape(alert.accountId())).append('\"')
42+
.append(",\"alertType\":\"").append(escape(alert.alertType())).append('\"')
43+
.append(",\"requestId\":\"").append(escape(alert.requestId())).append('\"')
44+
.append(",\"message\":\"").append(escape(alert.message())).append('\"')
45+
.append('}');
46+
}
47+
json.append(']');
48+
return BearValue.builder().put("alertsJson", json.toString()).build();
49+
}
50+
51+
private static String required(BearValue input, String field) {
52+
String value = input == null ? null : input.get(field);
53+
if (value == null || value.isBlank()) {
54+
throw new IllegalArgumentException(field + " is required");
55+
}
56+
return value;
57+
}
58+
59+
private static String escape(String value) {
60+
return value.replace("\\", "\\\\").replace("\"", "\\\"");
61+
}
62+
63+
private record AlertRecord(int alertSeq, String accountId, String alertType, String requestId, String message) {
64+
}
65+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package blocks.alert.log.impl;
2+
3+
import com.bear.generated.alert.log.AlertLogLogic;
4+
import com.bear.generated.alert.log.AlertLog_AppendAlertRequest;
5+
import com.bear.generated.alert.log.AlertLog_AppendAlertResult;
6+
import com.bear.generated.alert.log.AlertLog_GetAlertsRequest;
7+
import com.bear.generated.alert.log.AlertLog_GetAlertsResult;
8+
import com.bear.generated.alert.log.AlertStorePort;
9+
import com.bear.generated.alert.log.BearValue;
10+
11+
public final class AlertLogImpl implements AlertLogLogic {
12+
@Override
13+
public AlertLog_AppendAlertResult executeAppendAlert(AlertLog_AppendAlertRequest request, AlertStorePort alertStorePort) {
14+
requireNonBlank(request.getAccountId(), "accountId");
15+
requireNonBlank(request.getAlertType(), "alertType");
16+
requireNonBlank(request.getRequestId(), "requestId");
17+
requireNonBlank(request.getMessage(), "message");
18+
19+
BearValue stored = alertStorePort.append(BearValue.builder()
20+
.put("accountId", request.getAccountId())
21+
.put("alertType", request.getAlertType())
22+
.put("requestId", request.getRequestId())
23+
.put("message", request.getMessage())
24+
.build());
25+
return new AlertLog_AppendAlertResult(parseInt(stored.get("alertSeq"), "alertSeq"));
26+
}
27+
28+
@Override
29+
public AlertLog_GetAlertsResult executeGetAlerts(AlertLog_GetAlertsRequest request, AlertStorePort alertStorePort) {
30+
requireNonBlank(request.getAccountId(), "accountId");
31+
BearValue listed = alertStorePort.list(BearValue.builder()
32+
.put("accountId", request.getAccountId())
33+
.build());
34+
return new AlertLog_GetAlertsResult(defaultJson(listed.get("alertsJson")));
35+
}
36+
37+
private static void requireNonBlank(String value, String field) {
38+
if (value == null || value.isBlank()) {
39+
throw new IllegalArgumentException(field + " is required");
40+
}
41+
}
42+
43+
private static int parseInt(String raw, String field) {
44+
if (raw == null || raw.isBlank()) {
45+
throw new IllegalStateException(field + " is missing");
46+
}
47+
return Integer.parseInt(raw);
48+
}
49+
50+
private static String defaultJson(String json) {
51+
return json == null ? "[]" : json;
52+
}
53+
}

src/main/java/com/bear/account/demo/AccountApplication.java

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22

33
import blocks.account.adapter.InMemoryAccountStorePort;
44
import blocks.account.adapter.InMemoryIdempotencyPort;
5-
import blocks.account.impl.AccountNotFoundException;
6-
import blocks.account.impl.InsufficientFundsException;
7-
import blocks.transaction.log.adapter.InMemoryTransactionStorePort;
8-
5+
import blocks.alert.log.adapter.InMemoryAlertStorePort;
6+
import com.bear.generated.account.Account_AlertLogBlockClient;
97
import com.bear.generated.account.Account_CreateAccount;
108
import com.bear.generated.account.Account_CreateAccountRequest;
119
import com.bear.generated.account.Account_Deposit;
@@ -15,30 +13,40 @@
1513
import com.bear.generated.account.Account_TransactionLogBlockClient;
1614
import com.bear.generated.account.Account_Withdraw;
1715
import com.bear.generated.account.Account_WithdrawRequest;
16+
import com.bear.generated.account.AlertLogPort;
1817
import com.bear.generated.account.TransactionLogPort;
18+
import com.bear.generated.alert.log.AlertLog_AppendAlert;
19+
import com.bear.generated.alert.log.AlertLog_GetAlerts;
20+
import com.bear.generated.alert.log.AlertLog_GetAlertsRequest;
1921
import com.bear.generated.transaction.log.TransactionLog_AppendTransaction;
2022
import com.bear.generated.transaction.log.TransactionLog_GetTransactions;
2123
import com.bear.generated.transaction.log.TransactionLog_GetTransactionsRequest;
24+
import blocks.transaction.log.adapter.InMemoryTransactionStorePort;
2225

2326
public final class AccountApplication {
2427
private final Account_CreateAccount createAccount;
2528
private final Account_Deposit deposit;
2629
private final Account_Withdraw withdraw;
2730
private final Account_GetBalance getBalance;
2831
private final TransactionLog_GetTransactions getTransactions;
32+
private final AlertLog_GetAlerts getAlerts;
2933

3034
public AccountApplication() {
3135
InMemoryAccountStorePort accountStorePort = new InMemoryAccountStorePort();
3236
InMemoryIdempotencyPort idempotencyPort = new InMemoryIdempotencyPort();
3337
InMemoryTransactionStorePort transactionStorePort = new InMemoryTransactionStorePort();
38+
InMemoryAlertStorePort alertStorePort = new InMemoryAlertStorePort();
3439
TransactionLog_AppendTransaction appendTransaction = TransactionLog_AppendTransaction.of(transactionStorePort);
40+
AlertLog_AppendAlert appendAlert = AlertLog_AppendAlert.of(alertStorePort);
3541
TransactionLogPort transactionLogPort = new Account_TransactionLogBlockClient(appendTransaction);
42+
AlertLogPort alertLogPort = new Account_AlertLogBlockClient(appendAlert);
3643

37-
this.createAccount = Account_CreateAccount.of(accountStorePort, idempotencyPort, transactionLogPort);
38-
this.deposit = Account_Deposit.of(accountStorePort, idempotencyPort, transactionLogPort);
39-
this.withdraw = Account_Withdraw.of(accountStorePort, idempotencyPort, transactionLogPort);
40-
this.getBalance = Account_GetBalance.of(accountStorePort, idempotencyPort, transactionLogPort);
44+
this.createAccount = Account_CreateAccount.of(accountStorePort, alertLogPort, idempotencyPort, transactionLogPort);
45+
this.deposit = Account_Deposit.of(accountStorePort, alertLogPort, idempotencyPort, transactionLogPort);
46+
this.withdraw = Account_Withdraw.of(accountStorePort, alertLogPort, idempotencyPort, transactionLogPort);
47+
this.getBalance = Account_GetBalance.of(accountStorePort, alertLogPort, idempotencyPort, transactionLogPort);
4148
this.getTransactions = TransactionLog_GetTransactions.of(transactionStorePort);
49+
this.getAlerts = AlertLog_GetAlerts.of(alertStorePort);
4250
}
4351

4452
public String createAccount(String ownerId) {
@@ -62,6 +70,11 @@ public String getTransactionsJson(String accountId, Integer sinceSeq) {
6270
return getTransactions.execute(new TransactionLog_GetTransactionsRequest(accountId, sinceSeq)).getTransactionsJson();
6371
}
6472

73+
public String getAlertsJson(String accountId) {
74+
getBalance(accountId);
75+
return getAlerts.execute(new AlertLog_GetAlertsRequest(accountId)).getAlertsJson();
76+
}
77+
6578
private static OperationResult toOperationResult(com.bear.generated.account.Account_DepositResult result) {
6679
return new OperationResult(result.getBalanceCents(), result.getTxSeq());
6780
}
@@ -72,4 +85,4 @@ private static OperationResult toOperationResult(com.bear.generated.account.Acco
7285

7386
public record OperationResult(int balanceCents, int txSeq) {
7487
}
75-
}
88+
}

src/main/java/com/bear/account/demo/App.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ private void handle(HttpExchange exchange) throws IOException {
5656
handleGetTransactions(exchange, segments[2]);
5757
return;
5858
}
59+
if (segments.length == 4 && "accounts".equals(segments[1]) && "alerts".equals(segments[3]) && "GET".equals(method)) {
60+
handleGetAlerts(exchange, segments[2]);
61+
return;
62+
}
5963

6064
sendJson(exchange, 404, "{\"error\":\"not found\"}");
6165
} catch (AccountNotFoundException e) {
@@ -101,6 +105,11 @@ private void handleGetTransactions(HttpExchange exchange, String accountId) thro
101105
sendJson(exchange, 200, "{\"transactions\":" + transactionsJson + "}");
102106
}
103107

108+
private void handleGetAlerts(HttpExchange exchange, String accountId) throws IOException {
109+
String alertsJson = application.getAlertsJson(accountId);
110+
sendJson(exchange, 200, "{\"alerts\":" + alertsJson + "}");
111+
}
112+
104113
private static int readSinceSeq(String rawQuery) {
105114
if (rawQuery == null || rawQuery.isBlank()) {
106115
return 0;
@@ -148,4 +157,4 @@ private static Integer requireInt(String body, String field) {
148157
private static String escape(String value) {
149158
return value.replace("\\", "\\\\").replace("\"", "\\\"");
150159
}
151-
}
160+
}

0 commit comments

Comments
 (0)