Search for a command to run...
public class BillingTransactionInstance {
@Builder
public BillingTransactionInstance(...) {
...
}
@Transactional
public boolean commit() throws TransactionException {
var tx = this.getTransaction();
this.prep.setStatus("TX_START");
try {
// 일부러 Transaction 테이블은 수정 불가함 (DB에서 table update 권한 빼놓음)
// 그래서 저장시에 오류가 발생할 수 있음. 오류 발생시 tx 실패로 처리
transaction = this.transactionRepository.save(tx);
} catch (Exception e) {
throw TransactionException.DB_COMMIT_FAILED(e.getMessage() + "\r\n\r\n" + Arrays.stream(
e.getStackTrace()).map(StackTraceElement::toString).collect(Collectors.joining()),
tx.getId(), debitLedgerEntry, creditLedgerEntry);
}
debitLedgerEntry.commitDB(tx, ledgerEntryLinkRepository, ledgerEntryRepository,
billingAccountRepository);
creditLedgerEntry.commitDB(tx, ledgerEntryLinkRepository, ledgerEntryRepository,
billingAccountRepository);
this.prep.setStatus("OK");
return true;
}
private Transaction getTransaction() throws TransactionException {
if (transaction != null) {
throw TransactionException.ALREADY_COMMITED("", getTxId(), debitLedgerEntry, creditLedgerEntry);
}
var amountCredit = creditLedgerEntry.calculateSum(new HashMap<>(2));
var amountDebit = debitLedgerEntry.calculateSum(new HashMap<>(2));
if (!amountCredit.equals(amountDebit)) {
throw TransactionException.CREDIT_DEBIT_AMOUNT_MISMATCH(
"credit = " + amountCredit + " / debit = " + amountDebit, prep.getId(), debitLedgerEntry,
creditLedgerEntry);
}
if (mainCurrency == null) {
var maxBalance = BigDecimal.ZERO;
var maxCurrency = "";
for (Map.Entry<String, BigDecimal> amount : amountCredit.entrySet()) {
if (amount.getValue().compareTo(maxBalance) > 0) {
maxBalance = amount.getValue();
maxCurrency = amount.getKey();
}
}
mainCurrency = maxCurrency;
}
var amountPreview = exchangeService.exchange(mainCurrency, amountCredit);
this.transaction = Transaction.of(prep, this.mainCurrency, amountPreview, this.relatedTransaction,
null);
return this.transaction;
}
public long getTxId() {
return this.prep.getId();
}
@Transactional(Transactional.TxType.REQUIRES_NEW)
public void appendLog(String log) {
String existed_log = prep.getTxComment();
if (existed_log == null) {
existed_log = log;
} else {
existed_log = existed_log + "\r\n" + log;
}
this.prep.setTxComment(existed_log);
transactionPrepareRepository.save(this.prep);
}
@Transactional(Transactional.TxType.REQUIRES_NEW)
public void setStatus(String status) {
this.prep.setStatus(status);
transactionPrepareRepository.save(this.prep);
}
}
복식부기의 ledger는 계층 구조를 가지면 나중에 이해하기 쉬움. recursive 구조로 짜면 편함
public class BillingTransactionLedger extends LedgerTreeEntry<LedgerEntry> {
@Getter
protected final TransactionPrepare prep;
@Getter
@Setter
protected Transaction tx;
@Getter
protected LedgerEntryLink dbLinkEntity;
protected BillingTransactionLedger parent;
public BillingTransactionLedger(TransactionPrepare prep,
@Nullable BillingTransactionLedger parent, LedgerSide side) {
super(parent, side);
this.prep = prep;
}
protected BillingTransactionLedger(TransactionPrepare prep, @NonNull LedgerTreeEntry parent,
@NonNull LedgerTreeType link_type, @javax.annotation.Nullable String comment) {
super(parent, parent.getSide());
this.link_type = link_type;
this.comment = comment;
this.prep = prep;
}
public BillingTransactionLedger createSubTree(LedgerTreeType link_type, String comment) {
var tree = new BillingTransactionLedger(this.prep, this, link_type, comment);
ledgerTreeEntries.add(tree);
return tree;
}
/// 실제 entry를 저장함
private void commitEntry(LedgerEntryRepository repo,
BillingAccountRepository repo_account) {
for (LedgerEntry ledgerEntry : super.ledgerEntries) {
ledgerEntry.setEntryLink(this.dbLinkEntity);
var account = repo_account.findByIdForUpdate(ledgerEntry.getAccount().getId());
var accountBalance_before = account.getBalance();
var accountBalance_after = switch(this.dbLinkEntity.getSide()) {
case "C" -> accountBalance_before.subtract(ledgerEntry.getAmount());
case "D" -> accountBalance_before.add(ledgerEntry.getAmount());
default -> throw new IllegalStateException("Unexpected value: " + this.dbLinkEntity.getSide());
};
account.setBalance(accountBalance_after);
ledgerEntry.setAccountBalanceBefore(accountBalance_before);
ledgerEntry.setAccountBalanceAfter(accountBalance_after);
repo.save(ledgerEntry);
}
for (LedgerTreeEntry ledgerTreeEntry : (ledgerTreeEntries)) {
((BillingTransactionLedger)ledgerTreeEntry).commitEntry(repo, repo_account);
}
}
/// DB에 Tree 구조를 저장함. Tree 구조를 저장한 뒤에 실제 LedgerEntry를 저장 해야함.
private void commitTree(Transaction tx, LedgerEntryLinkRepository repo) {
this.tx = tx;
LedgerEntryLink parent_link = null;
if (this.parent != null) {
// root가 아니면 다 root거로 설정 받아옴
this.tx = parent.tx;
this.side = parent.side;
parent_link = this.parent.dbLinkEntity;
}
LedgerEntryLink ledgerEntryLink = new LedgerEntryLink(null, this.comment, this.link_type.name(),
this.tx, this.side.toString(), parent_link);
this.dbLinkEntity = repo.save(ledgerEntryLink);
for (LedgerTreeEntry ledgerTreeEntry : ledgerTreeEntries) {
((BillingTransactionLedger)ledgerTreeEntry).commitTree(tx, repo);
}
}
/// 바깥쪽에서 @Transactional 처리 필요. 최상위 root에서 호출돼야 함
public void commitDB(Transaction tx, LedgerEntryLinkRepository repo_link,
LedgerEntryRepository repo_entry, BillingAccountRepository repo_account) {
if (this.parent != null) {
// throw: root가 아님.
return;
}
this.tx = tx;
setLedgerFlags(this);
this.commitTree(tx, repo_link);
this.commitEntry(repo_entry, repo_account);
}
protected boolean setLedgerFlags(BillingTransactionLedger root) {
for (LedgerEntry ledgerEntry : ledgerEntries) {
ledgerEntry.setTx(this.tx);
ledgerEntry.setSide(this.side.toString());
}
for (LedgerTreeEntry ledgerTreeEntry : ledgerTreeEntries) {
((BillingTransactionLedger)ledgerTreeEntry).setLedgerFlags(root);
}
return true;
}
}
외부에서 진입은 따로 함
@Transactional(Transactional.TxType.REQUIRES_NEW)
public InvoiceCreatedResult declareInvoice(long amount, BillingAccount account, Organization org,
User user, String invoiceName, String invoiceDescription) {
var currency = account.getCurrency().trim();
BillingInvoice invoice = BillingInvoice.builder().org(org).user(user).status("START").txId(null).name(
invoiceName).totalPayAmount(BigDecimal.valueOf(amount)).currency(currency).account(account).build();
invoice = billingInvoiceRepository.save(invoice);
var pgResult = paymentGateway.createInvoice(currency, amount, invoice.getId().toString(),
invoiceName + "(" + invoiceDescription + ")", null);
invoice.setPgRef(pgResult.payment_name());
invoice.setPgRefId(pgResult.payment_id());
invoice.setPgLog(pgResult.log_data());
invoice.setStatus("PG_CREATE");
if (pgResult.success()) {
var txPrepare = declareTransaction(TransactionType.CREDIT_TOPUP, invoiceName, "", account);
invoice.setTxId(txPrepare.getTxId());
} else {
invoice.setStatus("PG_CREATE_FAIL");
}
billingInvoiceRepository.save(invoice);
return pgResult;
}
/// Transaction을 시작하려면 transaction_prepare를 만들어야 함.
/// transaction_prepare를 만들고 실제 Tx를 담당하는 객체 생성
public BillingTransactionInstance declareTransaction(TransactionType tx_type, String tx_name,
String prep_log, BillingAccount account) {
var prep = TransactionPrepare.builder()
.txType(tx_type.name())
.txName(tx_name)
.txComment(prep_log)
.status("START")
.issuedBy(account)
.build();
prep = transactionPrepareRepository.save(prep);
return BillingTransactionInstance.builder()
.billingAccountRepository(billingAccountRepository)
.transactionRepository(transactionRepository)
.transactionPrepareRepository(transactionPrepareRepository)
.ledgerEntryLinkRepository(ledgerEntryLinkRepository)
.ledgerEntryRepository(ledgerEntryRepository)
.prep(prep)
.build();
}
@Transactional(Transactional.TxType.REQUIRES_NEW)
public void invoicePaymentSuccess(long invoice_id) {
var invoice = this.billingInvoiceRepository.findById(invoice_id).orElseThrow(
() -> new RuntimeException("invoice not found"));
var tx = getTransaction(invoice.getTxId());
var account_pg = billingAccountRepository.findByAccountTypeAndAccountRef("PG",
paymentGateway.getPGNumber());
var account_target = billingAccountRepository.findByAccountTypeAndAccountRef("ORG",
invoice.getAccount().getId());
tx.getCreditLedgerEntry().addLedgerEntry(LedgerEntry.builder()
.account(account_pg)
// .side("C")
.amount(invoice.getTotalPayAmount())
.comment("invoice - " + invoice_id)
.build());
tx.getDebitLedgerEntry().addLedgerEntry(LedgerEntry.builder()
.account(account_target)
// .side("D")
.amount(invoice.getTotalPayAmount())
.comment("invoice - " + invoice_id)
.build());
tx.commit();
}