Skip to content

Commit e0c07ae

Browse files
committed
feat: add merchant settlement and reconciliation
1 parent 07f6cef commit e0c07ae

25 files changed

Lines changed: 1714 additions & 0 deletions

backend/src/main/java/com/payledger/platform/ledger/application/LedgerAccountService.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package com.payledger.platform.ledger.application;
22

33
import com.payledger.platform.ledger.domain.LedgerAccount;
4+
import com.payledger.platform.ledger.domain.LedgerAccountType;
45
import com.payledger.platform.ledger.infrastructure.LedgerAccountRepository;
56
import com.payledger.platform.shared.error.ResourceNotFoundException;
67
import com.payledger.platform.wallet.domain.Wallet;
78
import org.springframework.stereotype.Service;
89
import org.springframework.transaction.annotation.Transactional;
910

11+
import java.util.Locale;
1012
import java.util.UUID;
1113

1214
@Service
@@ -68,4 +70,20 @@ public LedgerAccount getForMerchantPayable(
6870
+ merchantId
6971
));
7072
}
73+
74+
@Transactional
75+
public LedgerAccount getOrCreateSettlementClearing(String currency) {
76+
String normalizedCurrency = currency.trim().toUpperCase(Locale.ROOT);
77+
String accountCode = "SETTLEMENT_CLEARING_" + normalizedCurrency;
78+
79+
return ledgerAccountRepository
80+
.findByAccountCode(accountCode)
81+
.orElseGet(() -> ledgerAccountRepository.saveAndFlush(
82+
LedgerAccount.createPlatformAccount(
83+
accountCode,
84+
LedgerAccountType.ASSET,
85+
normalizedCurrency
86+
)
87+
));
88+
}
7189
}

backend/src/main/java/com/payledger/platform/ledger/infrastructure/LedgerAccountRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public interface LedgerAccountRepository extends JpaRepository<LedgerAccount, UU
1414

1515
Optional<LedgerAccount> findByWalletId(UUID walletId);
1616

17+
Optional<LedgerAccount> findByAccountCode(String accountCode);
18+
1719
Optional<LedgerAccount> findByMerchantIdAndCurrency(
1820
UUID merchantId,
1921
String currency
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.payledger.platform.operations.api;
2+
3+
import com.payledger.platform.operations.application.CurrentOperationActorService;
4+
import com.payledger.platform.operations.application.OperationActor;
5+
import com.payledger.platform.settlement.api.CreateSettlementBatchRequest;
6+
import com.payledger.platform.settlement.api.ReconcileSettlementRequest;
7+
import com.payledger.platform.settlement.api.ReconciliationCaseResponse;
8+
import com.payledger.platform.settlement.api.SettlementBatchResponse;
9+
import com.payledger.platform.settlement.application.CreateSettlementBatchCommand;
10+
import com.payledger.platform.settlement.application.ReconcileSettlementCommand;
11+
import com.payledger.platform.settlement.application.ReconciliationCaseDetails;
12+
import com.payledger.platform.settlement.application.SettlementBatchDetails;
13+
import com.payledger.platform.settlement.application.SettlementService;
14+
import jakarta.validation.Valid;
15+
import org.springframework.http.HttpStatus;
16+
import org.springframework.security.access.prepost.PreAuthorize;
17+
import org.springframework.web.bind.annotation.PathVariable;
18+
import org.springframework.web.bind.annotation.PostMapping;
19+
import org.springframework.web.bind.annotation.RequestBody;
20+
import org.springframework.web.bind.annotation.RequestHeader;
21+
import org.springframework.web.bind.annotation.RequestMapping;
22+
import org.springframework.web.bind.annotation.ResponseStatus;
23+
import org.springframework.web.bind.annotation.RestController;
24+
25+
import java.util.UUID;
26+
27+
@RestController
28+
@RequestMapping("/api/v1/operations/settlements")
29+
@PreAuthorize("hasRole('OPERATIONS')")
30+
public class OperationsSettlementController {
31+
32+
private final SettlementService settlementService;
33+
private final CurrentOperationActorService currentActorService;
34+
35+
public OperationsSettlementController(
36+
SettlementService settlementService,
37+
CurrentOperationActorService currentActorService
38+
) {
39+
this.settlementService = settlementService;
40+
this.currentActorService = currentActorService;
41+
}
42+
43+
@PostMapping
44+
@ResponseStatus(HttpStatus.CREATED)
45+
public SettlementBatchResponse createBatch(
46+
@RequestHeader("Idempotency-Key") String idempotencyKey,
47+
@Valid @RequestBody CreateSettlementBatchRequest request
48+
) {
49+
OperationActor actor = currentActorService.getCurrentActor();
50+
SettlementBatchDetails details = settlementService.createBatch(
51+
new CreateSettlementBatchCommand(
52+
request.merchantId(),
53+
request.currency(),
54+
idempotencyKey,
55+
actor.externalSubject(),
56+
request.reason()
57+
)
58+
);
59+
60+
return SettlementBatchResponse.from(details);
61+
}
62+
63+
@PostMapping("/{settlementBatchId}/reconcile")
64+
public ReconciliationCaseResponse reconcile(
65+
@PathVariable UUID settlementBatchId,
66+
@Valid @RequestBody ReconcileSettlementRequest request
67+
) {
68+
OperationActor actor = currentActorService.getCurrentActor();
69+
ReconciliationCaseDetails details = settlementService.reconcile(
70+
new ReconcileSettlementCommand(
71+
settlementBatchId,
72+
request.providerReference(),
73+
request.actualAmountMinor(),
74+
request.actualCurrency(),
75+
actor.externalSubject()
76+
)
77+
);
78+
79+
return ReconciliationCaseResponse.from(details);
80+
}
81+
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package com.payledger.platform.payment.infrastructure;
22

33
import com.payledger.platform.payment.domain.PaymentIntent;
4+
import com.payledger.platform.payment.domain.PaymentIntentStatus;
45
import jakarta.persistence.LockModeType;
56
import org.springframework.data.jpa.repository.JpaRepository;
67
import org.springframework.data.jpa.repository.Lock;
78
import org.springframework.data.jpa.repository.Query;
89
import org.springframework.data.repository.query.Param;
910

11+
import java.util.List;
1012
import java.util.Optional;
1113
import java.util.UUID;
1214

@@ -27,4 +29,25 @@ Optional<PaymentIntent> findByCustomerIdAndIdempotencyKey(
2729
Optional<PaymentIntent> findByIdForUpdate(
2830
@Param("paymentIntentId") UUID paymentIntentId
2931
);
32+
33+
@Lock(LockModeType.PESSIMISTIC_WRITE)
34+
@Query("""
35+
SELECT intent
36+
FROM PaymentIntent intent
37+
WHERE intent.merchantId = :merchantId
38+
AND intent.currency = :currency
39+
AND intent.status = :status
40+
AND intent.captureJournalEntryId IS NOT NULL
41+
AND NOT EXISTS (
42+
SELECT line.id
43+
FROM SettlementLine line
44+
WHERE line.paymentIntentId = intent.id
45+
)
46+
ORDER BY intent.capturedAt ASC, intent.id ASC
47+
""")
48+
List<PaymentIntent> findUnsettledCapturedForUpdate(
49+
@Param("merchantId") UUID merchantId,
50+
@Param("currency") String currency,
51+
@Param("status") PaymentIntentStatus status
52+
);
3053
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.payledger.platform.settlement.api;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.NotNull;
5+
import jakarta.validation.constraints.Size;
6+
7+
import java.util.UUID;
8+
9+
public record CreateSettlementBatchRequest(
10+
@NotNull UUID merchantId,
11+
@NotBlank @Size(min = 3, max = 3) String currency,
12+
@NotBlank @Size(max = 500) String reason
13+
) {
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.payledger.platform.settlement.api;
2+
3+
import jakarta.validation.constraints.Min;
4+
import jakarta.validation.constraints.NotBlank;
5+
import jakarta.validation.constraints.Size;
6+
7+
public record ReconcileSettlementRequest(
8+
@NotBlank @Size(max = 120) String providerReference,
9+
@Min(0) long actualAmountMinor,
10+
@NotBlank @Size(min = 3, max = 3) String actualCurrency
11+
) {
12+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.payledger.platform.settlement.api;
2+
3+
import com.payledger.platform.settlement.application.ReconciliationCaseDetails;
4+
5+
import java.util.UUID;
6+
7+
public record ReconciliationCaseResponse(
8+
UUID id,
9+
String status,
10+
long expectedAmountMinor,
11+
long actualAmountMinor,
12+
String expectedCurrency,
13+
String actualCurrency,
14+
String discrepancyReason
15+
) {
16+
17+
public static ReconciliationCaseResponse from(
18+
ReconciliationCaseDetails details
19+
) {
20+
return new ReconciliationCaseResponse(
21+
details.id(),
22+
details.status(),
23+
details.expectedAmountMinor(),
24+
details.actualAmountMinor(),
25+
details.expectedCurrency(),
26+
details.actualCurrency(),
27+
details.discrepancyReason()
28+
);
29+
}
30+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.payledger.platform.settlement.api;
2+
3+
import com.payledger.platform.settlement.application.SettlementBatchDetails;
4+
5+
import java.util.UUID;
6+
7+
public record SettlementBatchResponse(
8+
UUID id,
9+
UUID merchantId,
10+
String currency,
11+
String status,
12+
long totalAmountMinor,
13+
UUID journalEntryId
14+
) {
15+
16+
public static SettlementBatchResponse from(
17+
SettlementBatchDetails details
18+
) {
19+
return new SettlementBatchResponse(
20+
details.id(),
21+
details.merchantId(),
22+
details.currency(),
23+
details.status(),
24+
details.totalAmountMinor(),
25+
details.journalEntryId()
26+
);
27+
}
28+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.payledger.platform.settlement.application;
2+
3+
import java.util.UUID;
4+
5+
public record CreateSettlementBatchCommand(
6+
UUID merchantId,
7+
String currency,
8+
String idempotencyKey,
9+
String actorExternalSubject,
10+
String reason
11+
) {
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.payledger.platform.settlement.application;
2+
3+
import java.util.UUID;
4+
5+
public record ReconcileSettlementCommand(
6+
UUID settlementBatchId,
7+
String providerReference,
8+
long actualAmountMinor,
9+
String actualCurrency,
10+
String actorExternalSubject
11+
) {
12+
}

0 commit comments

Comments
 (0)