Skip to content

Commit ec21c0b

Browse files
committed
feat: add transaction risk controls
1 parent e0c07ae commit ec21c0b

11 files changed

Lines changed: 911 additions & 3 deletions

File tree

backend/src/main/java/com/payledger/platform/payment/application/PaymentIntentService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,8 @@ private void requireRiskApproval(CreatePaymentIntentCommand command) {
393393
command.merchantId(),
394394
command.amountMinor(),
395395
command.currency()
396-
)
396+
),
397+
command.externalSubject()
397398
);
398399

399400
if (!decision.allowed()) {

backend/src/main/java/com/payledger/platform/payment/infrastructure/PaymentIntentRepository.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.springframework.data.jpa.repository.Query;
99
import org.springframework.data.repository.query.Param;
1010

11+
import java.time.Instant;
1112
import java.util.List;
1213
import java.util.Optional;
1314
import java.util.UUID;
@@ -50,4 +51,38 @@ List<PaymentIntent> findUnsettledCapturedForUpdate(
5051
@Param("currency") String currency,
5152
@Param("status") PaymentIntentStatus status
5253
);
54+
55+
@Query("""
56+
SELECT COALESCE(SUM(intent.amountMinor), 0)
57+
FROM PaymentIntent intent
58+
WHERE intent.customerId = :customerId
59+
AND intent.currency = :currency
60+
AND intent.status IN :statuses
61+
AND intent.createdAt >= :startInclusive
62+
AND intent.createdAt < :endExclusive
63+
""")
64+
long sumOutgoingAmountForCustomer(
65+
@Param("customerId") UUID customerId,
66+
@Param("currency") String currency,
67+
@Param("statuses") List<PaymentIntentStatus> statuses,
68+
@Param("startInclusive") Instant startInclusive,
69+
@Param("endExclusive") Instant endExclusive
70+
);
71+
72+
@Query("""
73+
SELECT COUNT(intent)
74+
FROM PaymentIntent intent
75+
WHERE intent.customerId = :customerId
76+
AND intent.currency = :currency
77+
AND intent.status IN :statuses
78+
AND intent.createdAt >= :startInclusive
79+
AND intent.createdAt < :endExclusive
80+
""")
81+
long countOutgoingForCustomer(
82+
@Param("customerId") UUID customerId,
83+
@Param("currency") String currency,
84+
@Param("statuses") List<PaymentIntentStatus> statuses,
85+
@Param("startInclusive") Instant startInclusive,
86+
@Param("endExclusive") Instant endExclusive
87+
);
5388
}

backend/src/main/java/com/payledger/platform/risk/application/RiskDecision.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,8 @@ public record RiskDecision(
77
public static RiskDecision allow() {
88
return new RiskDecision(true, "ALLOW");
99
}
10+
11+
public static RiskDecision deny(String reasonCode) {
12+
return new RiskDecision(false, reasonCode);
13+
}
1014
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.payledger.platform.risk.application;
2+
3+
import com.payledger.platform.audit.application.AuditEventCommand;
4+
import com.payledger.platform.audit.application.AuditEventService;
5+
import org.springframework.stereotype.Service;
6+
import org.springframework.transaction.annotation.Propagation;
7+
import org.springframework.transaction.annotation.Transactional;
8+
9+
import java.util.Map;
10+
import java.util.UUID;
11+
12+
@Service
13+
public class RiskDecisionAuditService {
14+
15+
private final AuditEventService auditEventService;
16+
17+
public RiskDecisionAuditService(AuditEventService auditEventService) {
18+
this.auditEventService = auditEventService;
19+
}
20+
21+
@Transactional(propagation = Propagation.REQUIRES_NEW)
22+
public void recordDenial(
23+
String action,
24+
UUID customerId,
25+
UUID sourceWalletId,
26+
UUID destinationWalletId,
27+
UUID merchantId,
28+
long amountMinor,
29+
String currency,
30+
String actorExternalSubject,
31+
String reasonCode
32+
) {
33+
auditEventService.record(
34+
new AuditEventCommand(
35+
"RISK_DECISION_DENIED",
36+
actorExternalSubject,
37+
customerId,
38+
"RISK_DECISION",
39+
UUID.randomUUID(),
40+
Map.of(
41+
"action", action,
42+
"reasonCode", reasonCode,
43+
"customerId", customerId.toString(),
44+
"sourceWalletId", sourceWalletId.toString(),
45+
"destinationWalletId", destinationWalletId == null
46+
? ""
47+
: destinationWalletId.toString(),
48+
"merchantId", merchantId == null
49+
? ""
50+
: merchantId.toString(),
51+
"amountMinor", amountMinor,
52+
"currency", currency
53+
)
54+
)
55+
);
56+
}
57+
}

0 commit comments

Comments
 (0)