diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/JournalEntryAccountDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/JournalEntryAccountDto.java new file mode 100644 index 0000000..2dd9c0a --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/erp/JournalEntryAccountDto.java @@ -0,0 +1,27 @@ +package zw.qantra.tm.domain.dtos.erp; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class JournalEntryAccountDto { + private String doctype; + private String account; + @JsonProperty("debit_in_account_currency") + private BigDecimal debitInAccountCurrency; + @JsonProperty("credit_in_account_currency") + private BigDecimal creditInAccountCurrency; + @JsonProperty("cost_center") + private String costCenter; + @JsonProperty("party_type") + private String partyType; + private String party; +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/JournalEntryDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/JournalEntryDto.java new file mode 100644 index 0000000..1feb682 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/erp/JournalEntryDto.java @@ -0,0 +1,22 @@ +package zw.qantra.tm.domain.dtos.erp; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class JournalEntryDto { + private String doctype; + private String company; + @JsonProperty("posting_date") + private String postingDate; + private int docstatus; + private List accounts; +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/PaymentEntryDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/PaymentEntryDto.java new file mode 100644 index 0000000..df13ee3 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/erp/PaymentEntryDto.java @@ -0,0 +1,40 @@ +package zw.qantra.tm.domain.dtos.erp; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PaymentEntryDto { + private String doctype; + private String company; + @JsonProperty("payment_type") + private String paymentType; + @JsonProperty("party_type") + private String partyType; + private String party; + @JsonProperty("paid_from") + private String paidFrom; + @JsonProperty("paid_to") + private String paidTo; + @JsonProperty("paid_amount") + private BigDecimal paidAmount; + @JsonProperty("received_amount") + private BigDecimal receivedAmount; + @JsonProperty("posting_date") + private String postingDate; + @JsonProperty("reference_no") + private String referenceNo; + @JsonProperty("reference_date") + private String referenceDate; + private List references; + private int docstatus; +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/PaymentReferenceDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/PaymentReferenceDto.java new file mode 100644 index 0000000..9ec07ff --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/erp/PaymentReferenceDto.java @@ -0,0 +1,22 @@ +package zw.qantra.tm.domain.dtos.erp; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PaymentReferenceDto { + @JsonProperty("reference_doctype") + private String referenceDoctype; + @JsonProperty("reference_name") + private String referenceName; + @JsonProperty("allocated_amount") + private BigDecimal allocatedAmount; +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/PurchaseInvoiceDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/PurchaseInvoiceDto.java new file mode 100644 index 0000000..9609b1b --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/erp/PurchaseInvoiceDto.java @@ -0,0 +1,39 @@ +package zw.qantra.tm.domain.dtos.erp; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PurchaseInvoiceDto { + + private String doctype; + private String supplier; + @JsonProperty("posting_date") + private String postingDate; + @JsonProperty("due_date") + private String dueDate; + private String company; + @JsonProperty("is_return") + private int isReturn; + private String currency; + @JsonProperty("conversion_rate") + private int conversionRate; + @JsonProperty("net_total") + private BigDecimal netTotal; + @JsonProperty("grand_total") + private BigDecimal grandTotal; + @JsonProperty("is_paid") + private int isPaid; + private int docstatus; + private List items; + private List taxes; +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/PurchaseInvoiceItemDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/PurchaseInvoiceItemDto.java new file mode 100644 index 0000000..f708df7 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/erp/PurchaseInvoiceItemDto.java @@ -0,0 +1,26 @@ +package zw.qantra.tm.domain.dtos.erp; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PurchaseInvoiceItemDto { + + private String doctype; + @JsonProperty("item_code") + private String itemCode; + @JsonProperty("item_name") + private String itemName; + private BigDecimal qty; + private int rate; + private BigDecimal amount; + private String uom; +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/PurchaseTaxDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/PurchaseTaxDto.java new file mode 100644 index 0000000..efd2b7c --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/erp/PurchaseTaxDto.java @@ -0,0 +1,29 @@ +package zw.qantra.tm.domain.dtos.erp; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PurchaseTaxDto { + + private String doctype; + @JsonProperty("charge_type") + private String chargeType; + @JsonProperty("account_head") + private String accountHead; + private BigDecimal rate; + @JsonProperty("cost_center") + private String costCenter; + @JsonProperty("tax_amount") + private BigDecimal taxAmount; + private BigDecimal total; + private String description; +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/SalesInvoiceDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/SalesInvoiceDto.java new file mode 100644 index 0000000..5fe49a2 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/erp/SalesInvoiceDto.java @@ -0,0 +1,25 @@ +package zw.qantra.tm.domain.dtos.erp; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SalesInvoiceDto { + private String doctype; + private String customer; + @JsonProperty("posting_date") + private String postingDate; + @JsonProperty("due_date") + private String dueDate; + private String company; + private int docstatus; + private List items; +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/SalesInvoiceItemDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/SalesInvoiceItemDto.java new file mode 100644 index 0000000..710249f --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/erp/SalesInvoiceItemDto.java @@ -0,0 +1,21 @@ +package zw.qantra.tm.domain.dtos.erp; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SalesInvoiceItemDto { + @JsonProperty("item_code") + private String itemCode; + private int qty; + private BigDecimal rate; + private BigDecimal amount; +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/enums/RequestType.java b/src/main/java/zw/qantra/tm/domain/enums/RequestType.java index 83bb54a..26e2ac2 100644 --- a/src/main/java/zw/qantra/tm/domain/enums/RequestType.java +++ b/src/main/java/zw/qantra/tm/domain/enums/RequestType.java @@ -4,5 +4,10 @@ public enum RequestType { CONFIRM, REQUEST, POLL, - INTEGRATION + INTEGRATION, + SALES_INVOICE, + JOURNAL_ENTRY, + PURCHASE_PAYMENT, + PURCHASE_INVOICE, + SALES_PAYMENT } diff --git a/src/main/java/zw/qantra/tm/domain/repositories/UserRepository.java b/src/main/java/zw/qantra/tm/domain/repositories/UserRepository.java index 7fca923..c291093 100644 --- a/src/main/java/zw/qantra/tm/domain/repositories/UserRepository.java +++ b/src/main/java/zw/qantra/tm/domain/repositories/UserRepository.java @@ -5,9 +5,11 @@ import org.springframework.stereotype.Repository; import zw.qantra.tm.domain.models.User; import java.util.Optional; +import java.util.UUID; @Repository public interface UserRepository extends JpaRepository { + Optional findById(String uid); Optional findByUsername(String username); Optional findByEmail(String email); Optional findByPhone(String phone); diff --git a/src/main/java/zw/qantra/tm/domain/services/TransactionService.java b/src/main/java/zw/qantra/tm/domain/services/TransactionService.java index 4f6db75..d67ea6a 100644 --- a/src/main/java/zw/qantra/tm/domain/services/TransactionService.java +++ b/src/main/java/zw/qantra/tm/domain/services/TransactionService.java @@ -28,6 +28,8 @@ import zw.qantra.tm.domain.services.processors.handlers.FormattingHandler; import zw.qantra.tm.domain.services.processors.handlers.LogicPipeline; import zw.qantra.tm.domain.services.processors.handlers.RecipientsUpdateHandler; import zw.qantra.tm.domain.services.processors.handlers.ValidationHandler; +import zw.qantra.tm.domain.services.processors.handlers.erp.JournalEntryHandler; +import zw.qantra.tm.domain.services.processors.handlers.erp.PaymentEntryHandler; import zw.qantra.tm.domain.services.processors.handlers.erp.PurchaseInvoiceHandler; import zw.qantra.tm.domain.services.processors.handlers.erp.SalesInvoiceHandler; import zw.qantra.tm.domain.models.TransactionEvent; @@ -59,7 +61,9 @@ public class TransactionService { transactionEventService.save(transactionEvent); LogicPipeline transactionLogic = - new LogicPipeline<>(new ValidationHandler()); + new LogicPipeline<>(new ValidationHandler()) + .addHandler(new JournalEntryHandler()) // todo: do this in a separate thread + .addHandler(new PaymentEntryHandler()); // todo: do this in a separate thread transaction = transactionLogic.execute(transaction); Provider provider = providerService.getProviderRepository() @@ -90,7 +94,8 @@ public class TransactionService { // run post integration logic LogicPipeline postTransactionLogic = - new LogicPipeline<>(new PurchaseInvoiceHandler()); + new LogicPipeline<>(new PurchaseInvoiceHandler()) // todo: do this in a separate thread + .addHandler(new PurchaseInvoiceHandler()); // todo: do this in a separate thread transaction = postTransactionLogic.execute(transaction); transactionEvent.setStatus(transaction.getStatus()); @@ -199,7 +204,7 @@ public class TransactionService { // run post transaction logic LogicPipeline postTransactionLogic = new LogicPipeline<>(new RecipientsUpdateHandler(recipientService)) - .addHandler(new SalesInvoiceHandler()); + .addHandler(new SalesInvoiceHandler());// todo: do this in a separate thread transaction = postTransactionLogic.execute(transaction); transactionEvent.setStatus(transaction.getStatus()); diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/JournalEntryHandler.java b/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/JournalEntryHandler.java new file mode 100644 index 0000000..8092217 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/JournalEntryHandler.java @@ -0,0 +1,169 @@ +package zw.qantra.tm.domain.services.processors.handlers.erp; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import zw.qantra.tm.domain.dtos.erp.JournalEntryAccountDto; +import zw.qantra.tm.domain.dtos.erp.JournalEntryDto; +import zw.qantra.tm.domain.dtos.erp.SalesInvoiceDto; +import zw.qantra.tm.domain.dtos.erp.SalesInvoiceItemDto; +import zw.qantra.tm.domain.enums.RequestType; +import zw.qantra.tm.domain.enums.Status; +import zw.qantra.tm.domain.models.Transaction; +import zw.qantra.tm.domain.models.TransactionEvent; +import zw.qantra.tm.domain.models.User; +import zw.qantra.tm.domain.services.TransactionEventService; +import zw.qantra.tm.domain.services.TransactionService; +import zw.qantra.tm.domain.services.UserService; +import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; +import zw.qantra.tm.exceptions.ApiException; +import zw.qantra.tm.rest.RestService; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.Map; + +@Service +public class JournalEntryHandler implements LogicHandler{ + Logger logger = LoggerFactory.getLogger(JournalEntryHandler.class); + + @Autowired + private RestService restService; + @Autowired + private UserService userService; + @Autowired + private TransactionEventService transactionEventService; + @Autowired + private TransactionService transactionService; + + @Value("${erpnext.url}") + private String erpnextUrl; + @Value("${erpnext.api.key}") + private String erpnextApiKey; + @Value("${erpnext.api.secret}") + private String erpnextApiSecret; + @Value("${erpnext.username}") + private String erpnextUsername; + @Value("${erpnext.password}") + private String erpnextPassword; + @Value("${erpnext.company}") + private String erpnextCompany; + @Value("${erpnext.accounts.gatewayexpense}") + private String gatewayExpenseAccount; + @Value("${erpnext.accounts.accountsreceivable}") + private String accountsReceivableAccount; + @Value("${erpnext.accounts.cash}") + private String cashAccount; + @Value("${erpnext.cost.center}") + private String costCenter; + + @Override + public Transaction process(Transaction transaction) { + logger.info("Processing journal entry for transaction: {}", transaction.getReference()); + TransactionEvent transactionEvent = TransactionEvent.builder() + .transactionId(transaction.getId().toString()) + .event(RequestType.JOURNAL_ENTRY.name()) + .status(Status.PROCESSING) + .build(); + transactionEventService.save(transactionEvent); + + if(transaction.getStatus() == Status.SUCCESS){ + try { + logger.info("Creating journal entry in ERPNext for successful transaction"); + + // login to erp + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/json"); + headers.add("Accept", "application/json"); + + Map loginRequest = Map.of( + "usr", erpnextUsername, + "pwd", erpnextPassword); + + String sid = ""; + + try { + ResponseEntity response = restService.postAsEntity( + erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class); + + sid = response.getHeaders().getFirst("Set-Cookie"); + } catch (Exception e) { + e.printStackTrace(); + throw new ApiException("Failed to login to ERPNext"); + } + + // create customer in erp + String basicAuth = "Basic " + Base64.getEncoder().encodeToString( + (erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8)); + headers.add("Authorization", basicAuth); + headers.add("Cookie", sid); + + User user = userService.getUserRepository().findById(transaction.getUserId()).orElse(null); + if(user == null) + throw new ApiException("Could not find user"); + + JournalEntryAccountDto cashAccEntry = JournalEntryAccountDto.builder() + .doctype("Journal Entry Account") + .account(cashAccount) + .debitInAccountCurrency(transaction.getAmount()) + .costCenter(costCenter) + .build(); + JournalEntryAccountDto expenseEntry = JournalEntryAccountDto.builder() + .doctype("Journal Entry Account") + .account(gatewayExpenseAccount) + .debitInAccountCurrency(transaction.getGatewayCharge()) + .costCenter(costCenter) + .build(); + JournalEntryAccountDto accRecievable = JournalEntryAccountDto.builder() + .doctype("Journal Entry Account") + .account(accountsReceivableAccount) + .creditInAccountCurrency(transaction.getAmount().add(transaction.getGatewayCharge())) + .party("Customer") + .costCenter(costCenter) + .build(); + + JournalEntryDto journalEntryDto = JournalEntryDto.builder() + .doctype("Journal Entry") + .company(erpnextCompany) + .postingDate(transaction.getCreatedAt().toLocalDate().toString()) + .docstatus(1) + .accounts(Arrays.asList(cashAccEntry, expenseEntry, accRecievable)) + .build(); + + try { + LinkedHashMap response = restService.postAs( + erpnextUrl + "/v2/document/Journal Entry", journalEntryDto, headers, LinkedHashMap.class); + + LinkedHashMap data = (LinkedHashMap) response.get("data"); + String transactionId = (String) data.get("name"); + + transaction.setErpJournalRef(transactionId); + transactionEvent.setStatus(Status.SUCCESS); + } catch (Exception e) { + e.printStackTrace(); + throw new ApiException("Failed to create journal entry in ERPNext"); + } + + logger.info("journal entry created successfully in ERPNext"); + } catch (Exception e) { + logger.error("Failed to create journal entry in ERPNext: {}", e.getMessage(), e); + // Don't fail the transaction, just log the error + // The ERP invoice creation is a side effect, not critical to the payment flow + transactionEvent.setStatus(Status.FAILED); + transactionEvent.setMessage(e.getMessage()); + } + } else { + logger.info("Transaction status is not SUCCESS ({}), skipping journal entry creation", transaction.getStatus()); + } + + transactionEventService.save(transactionEvent); + + return transactionService.save(transaction); + } +} diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/PaymentEntryHandler.java b/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/PaymentEntryHandler.java new file mode 100644 index 0000000..f5d80c5 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/PaymentEntryHandler.java @@ -0,0 +1,161 @@ +package zw.qantra.tm.domain.services.processors.handlers.erp; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import zw.qantra.tm.domain.dtos.erp.PaymentEntryDto; +import zw.qantra.tm.domain.dtos.erp.PaymentReferenceDto; +import zw.qantra.tm.domain.enums.RequestType; +import zw.qantra.tm.domain.enums.Status; +import zw.qantra.tm.domain.models.Transaction; +import zw.qantra.tm.domain.models.TransactionEvent; +import zw.qantra.tm.domain.models.User; +import zw.qantra.tm.domain.services.TransactionEventService; +import zw.qantra.tm.domain.services.TransactionService; +import zw.qantra.tm.domain.services.UserService; +import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; +import zw.qantra.tm.exceptions.ApiException; +import zw.qantra.tm.rest.RestService; + +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.Map; + +@Service +public class PaymentEntryHandler implements LogicHandler{ + Logger logger = LoggerFactory.getLogger(PaymentEntryHandler.class); + + @Autowired + private RestService restService; + @Autowired + private UserService userService; + @Autowired + private TransactionEventService transactionEventService; + @Autowired + private TransactionService transactionService; + + @Value("${erpnext.url}") + private String erpnextUrl; + @Value("${erpnext.api.key}") + private String erpnextApiKey; + @Value("${erpnext.api.secret}") + private String erpnextApiSecret; + @Value("${erpnext.username}") + private String erpnextUsername; + @Value("${erpnext.password}") + private String erpnextPassword; + @Value("${erpnext.company}") + private String erpnextCompany; + @Value("${erpnext.accounts.accountsreceivable}") + private String accountsReceivableAccount; + @Value("${erpnext.accounts.cash}") + private String cashAccount; + + @Override + public Transaction process(Transaction transaction) { + logger.info("Processing payment entry for transaction: {}", transaction.getReference()); + TransactionEvent transactionEvent = TransactionEvent.builder() + .transactionId(transaction.getId().toString()) + .event(RequestType.SALES_PAYMENT.name()) + .status(Status.PROCESSING) + .build(); + transactionEventService.save(transactionEvent); + + if(transaction.getStatus() == Status.SUCCESS){ + try { + logger.info("Creating payment entry in ERPNext for successful transaction"); + + // login to erp + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/json"); + headers.add("Accept", "application/json"); + + Map loginRequest = Map.of( + "usr", erpnextUsername, + "pwd", erpnextPassword); + + String sid = ""; + + try { + ResponseEntity response = restService.postAsEntity( + erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class); + + sid = response.getHeaders().getFirst("Set-Cookie"); + } catch (Exception e) { + e.printStackTrace(); + throw new ApiException("Failed to login to ERPNext"); + } + + // create customer in erp + String basicAuth = "Basic " + Base64.getEncoder().encodeToString( + (erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8)); + headers.add("Authorization", basicAuth); + headers.add("Cookie", sid); + + User user = userService.getUserRepository().findById(transaction.getUserId()).orElse(null); + if(user == null) + throw new ApiException("Could not find user"); + + BigDecimal amount = transaction.getAmount().add(transaction.getGatewayCharge()); + + PaymentReferenceDto paymentReferenceDto = PaymentReferenceDto.builder() + .referenceDoctype("Sales Invoice") + .referenceName(transaction.getErpSalesRef()) + .allocatedAmount(amount) + .build(); + + PaymentEntryDto paymentEntryDto = PaymentEntryDto.builder() + .doctype("Payment Entry") + .company(erpnextCompany) + .docstatus(1) + .paymentType("Receive") + .party(user.getErpCustomerRef()) + .partyType("Customer") + .paidFrom(accountsReceivableAccount) + .paidTo(cashAccount) + .paidAmount(amount) + .receivedAmount(amount) + .referenceNo(transaction.getDebitRef()) + .referenceDate(transaction.getCreatedAt().toLocalDate().toString()) + .postingDate(transaction.getCreatedAt().toLocalDate().toString()) + .references(Arrays.asList(paymentReferenceDto)) + .build(); + + try { + LinkedHashMap response = restService.postAs( + erpnextUrl + "/v2/document/Payment Entry", paymentEntryDto, headers, LinkedHashMap.class); + + LinkedHashMap data = (LinkedHashMap) response.get("data"); + String transactionId = (String) data.get("name"); + + transaction.setErpSalesPaymentRef(transactionId); + transactionEvent.setStatus(Status.SUCCESS); + } catch (Exception e) { + e.printStackTrace(); + throw new ApiException("Failed to create payment entry in ERPNext"); + } + + logger.info("payment entry created successfully in ERPNext"); + } catch (Exception e) { + logger.error("Failed to create payment entry in ERPNext: {}", e.getMessage(), e); + // Don't fail the transaction, just log the error + // The ERP invoice creation is a side effect, not critical to the payment flow + transactionEvent.setStatus(Status.FAILED); + transactionEvent.setMessage(e.getMessage()); + } + } else { + logger.info("Transaction status is not SUCCESS ({}), skipping payment entry creation", transaction.getStatus()); + } + + transactionEventService.save(transactionEvent); + + return transactionService.save(transaction); + } +} diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/PurchaseInvoiceHandler.java b/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/PurchaseInvoiceHandler.java index 98be333..b107e6d 100644 --- a/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/PurchaseInvoiceHandler.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/PurchaseInvoiceHandler.java @@ -1,21 +1,169 @@ package zw.qantra.tm.domain.services.processors.handlers.erp; -import org.springframework.stereotype.Service; - -import lombok.RequiredArgsConstructor; -import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; -import zw.qantra.tm.domain.models.Transaction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import zw.qantra.tm.domain.dtos.erp.*; +import zw.qantra.tm.domain.enums.RequestType; +import zw.qantra.tm.domain.enums.Status; +import zw.qantra.tm.domain.models.Transaction; +import zw.qantra.tm.domain.models.TransactionEvent; +import zw.qantra.tm.domain.models.User; +import zw.qantra.tm.domain.services.TransactionEventService; +import zw.qantra.tm.domain.services.TransactionService; +import zw.qantra.tm.domain.services.UserService; +import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; +import zw.qantra.tm.exceptions.ApiException; +import zw.qantra.tm.rest.RestService; + +import java.nio.charset.StandardCharsets; +import java.util.*; @Service -@RequiredArgsConstructor public class PurchaseInvoiceHandler implements LogicHandler{ Logger logger = LoggerFactory.getLogger(PurchaseInvoiceHandler.class); + @Autowired + private RestService restService; + @Autowired + private UserService userService; + @Autowired + private TransactionEventService transactionEventService; + @Autowired + private TransactionService transactionService; + + @Value("${erpnext.url}") + private String erpnextUrl; + @Value("${erpnext.api.key}") + private String erpnextApiKey; + @Value("${erpnext.api.secret}") + private String erpnextApiSecret; + @Value("${erpnext.username}") + private String erpnextUsername; + @Value("${erpnext.password}") + private String erpnextPassword; + @Value("${erpnext.company}") + private String erpnextCompany; + @Value("${erpnext.cost.center}") + private String erpnextCostCenter; + @Value("${erpnext.accounts.commission}") + private String erpnextCommissionAccount; + @Override public Transaction process(Transaction transaction) { - logger.info("processing purchase invoice"); - return transaction; + logger.info("Processing purchase invoice for transaction: {}", transaction.getReference()); + TransactionEvent transactionEvent = TransactionEvent.builder() + .transactionId(transaction.getId().toString()) + .event(RequestType.PURCHASE_INVOICE.name()) + .status(Status.PROCESSING) + .build(); + transactionEventService.save(transactionEvent); + + if(transaction.getStatus() == Status.SUCCESS){ + try { + logger.info("Creating purchase invoice in ERPNext for successful transaction"); + + // login to erp + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/json"); + headers.add("Accept", "application/json"); + + Map loginRequest = Map.of( + "usr", erpnextUsername, + "pwd", erpnextPassword); + + String sid = ""; + + try { + ResponseEntity response = restService.postAsEntity( + erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class); + + sid = response.getHeaders().getFirst("Set-Cookie"); + } catch (Exception e) { + e.printStackTrace(); + throw new ApiException("Failed to login to ERPNext"); + } + + // create customer in erp + String basicAuth = "Basic " + Base64.getEncoder().encodeToString( + (erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8)); + headers.add("Authorization", basicAuth); + headers.add("Cookie", sid); + + User user = userService.getUserRepository().findById(transaction.getUserId()).orElse(null); + if(user == null) + throw new ApiException("Could not find user"); + + PurchaseInvoiceItemDto purchaseInvoiceItemDto = PurchaseInvoiceItemDto.builder() + .doctype("Purchase Invoice Item") + .uom("Unit") + .itemCode(transaction.getBillName()) + .itemName(transaction.getBillName()) + .qty(transaction.getAmount()) + .rate(1) + .amount(transaction.getAmount()) + .build(); + + PurchaseTaxDto purchaseTaxDto = PurchaseTaxDto.builder() + .doctype("Purchase Tax") + .chargeType("On Net Total") + .accountHead(erpnextCommissionAccount) + .rate(transaction.getGatewayCharge().negate()) + .costCenter(erpnextCostCenter) + .taxAmount(transaction.getGatewayCharge().negate()) + .total(transaction.getAmount().subtract(transaction.getGatewayCharge())) + .description("Discounts Received") + .build(); + + PurchaseInvoiceDto purchaseInvoiceDto = PurchaseInvoiceDto.builder() + .doctype("Purchase Invoice") + .supplier("Steward Bank") + .company(erpnextCompany) + .postingDate(transaction.getCreatedAt().toLocalDate().toString()) + .dueDate(transaction.getCreatedAt().toLocalDate().plusDays(1).toString()) + .docstatus(1) + .isReturn(0) + .isPaid(0) + .currency(transaction.getDebitCurrency().toString()) + .conversionRate(1) + .netTotal(transaction.getAmount().subtract(transaction.getGatewayCharge())) + .grandTotal(transaction.getAmount().subtract(transaction.getGatewayCharge())) + .items(Collections.singletonList(purchaseInvoiceItemDto)) + .taxes(Collections.singletonList(purchaseTaxDto)) + .build(); + + try { + LinkedHashMap response = restService.postAs( + erpnextUrl + "/v2/document/Purchase Invoice", purchaseInvoiceDto, headers, LinkedHashMap.class); + + LinkedHashMap data = (LinkedHashMap) response.get("data"); + String transactionId = (String) data.get("name"); + + transaction.setErpPurchaseRef(transactionId); + transactionEvent.setStatus(Status.SUCCESS); + } catch (Exception e) { + e.printStackTrace(); + throw new ApiException("Failed to create purchase invoice in ERPNext"); + } + + logger.info("purchase invoice created successfully in ERPNext"); + } catch (Exception e) { + logger.error("Failed to create purchase invoice in ERPNext: {}", e.getMessage(), e); + // Don't fail the transaction, just log the error + // The ERP invoice creation is a side effect, not critical to the payment flow + transactionEvent.setStatus(Status.FAILED); + transactionEvent.setMessage(e.getMessage()); + } + } else { + logger.info("Transaction status is not SUCCESS ({}), skipping purchase invoice creation", transaction.getStatus()); + } + + transactionEventService.save(transactionEvent); + + return transactionService.save(transaction); } } diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/PurchasePaymentEntryHandler.java b/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/PurchasePaymentEntryHandler.java new file mode 100644 index 0000000..20f4137 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/PurchasePaymentEntryHandler.java @@ -0,0 +1,163 @@ +package zw.qantra.tm.domain.services.processors.handlers.erp; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import zw.qantra.tm.domain.dtos.erp.PaymentEntryDto; +import zw.qantra.tm.domain.dtos.erp.PaymentReferenceDto; +import zw.qantra.tm.domain.enums.RequestType; +import zw.qantra.tm.domain.enums.Status; +import zw.qantra.tm.domain.models.Transaction; +import zw.qantra.tm.domain.models.TransactionEvent; +import zw.qantra.tm.domain.models.User; +import zw.qantra.tm.domain.services.TransactionEventService; +import zw.qantra.tm.domain.services.TransactionService; +import zw.qantra.tm.domain.services.UserService; +import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; +import zw.qantra.tm.exceptions.ApiException; +import zw.qantra.tm.rest.RestService; + +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.Map; + +@Service +public class PurchasePaymentEntryHandler implements LogicHandler{ + Logger logger = LoggerFactory.getLogger(PurchasePaymentEntryHandler.class); + + @Autowired + private RestService restService; + @Autowired + private UserService userService; + @Autowired + private TransactionEventService transactionEventService; + @Autowired + private TransactionService transactionService; + + @Value("${erpnext.url}") + private String erpnextUrl; + @Value("${erpnext.api.key}") + private String erpnextApiKey; + @Value("${erpnext.api.secret}") + private String erpnextApiSecret; + @Value("${erpnext.username}") + private String erpnextUsername; + @Value("${erpnext.password}") + private String erpnextPassword; + @Value("${erpnext.company}") + private String erpnextCompany; + @Value("${erpnext.accounts.accountsreceivable}") + private String accountsReceivableAccount; + @Value("${erpnext.accounts.cash}") + private String cashAccount; + @Value("erpnext.accounts.payable") + private String payableAccount; + + @Override + public Transaction process(Transaction transaction) { + logger.info("Processing purchase payment entry for transaction: {}", transaction.getReference()); + TransactionEvent transactionEvent = TransactionEvent.builder() + .transactionId(transaction.getId().toString()) + .event(RequestType.PURCHASE_PAYMENT.name()) + .status(Status.PROCESSING) + .build(); + transactionEventService.save(transactionEvent); + + if(transaction.getStatus() == Status.SUCCESS){ + try { + logger.info("Creating purchase payment entry in ERPNext for successful transaction"); + + // login to erp + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/json"); + headers.add("Accept", "application/json"); + + Map loginRequest = Map.of( + "usr", erpnextUsername, + "pwd", erpnextPassword); + + String sid = ""; + + try { + ResponseEntity response = restService.postAsEntity( + erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class); + + sid = response.getHeaders().getFirst("Set-Cookie"); + } catch (Exception e) { + e.printStackTrace(); + throw new ApiException("Failed to login to ERPNext"); + } + + // create customer in erp + String basicAuth = "Basic " + Base64.getEncoder().encodeToString( + (erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8)); + headers.add("Authorization", basicAuth); + headers.add("Cookie", sid); + + User user = userService.getUserRepository().findById(transaction.getUserId()).orElse(null); + if(user == null) + throw new ApiException("Could not find user"); + + BigDecimal amount = transaction.getAmount().subtract(transaction.getGatewayCharge()); + + PaymentReferenceDto paymentReferenceDto = PaymentReferenceDto.builder() + .referenceDoctype("Purchase Invoice") + .referenceName(transaction.getErpPurchaseRef()) + .allocatedAmount(amount) + .build(); + + PaymentEntryDto paymentEntryDto = PaymentEntryDto.builder() + .doctype("Payment Entry") + .company(erpnextCompany) + .docstatus(1) + .paymentType("Pay") + .party("Steward Bank") + .partyType("Supplier") + .paidFrom(cashAccount) + .paidTo(payableAccount) + .paidAmount(amount) + .receivedAmount(amount) + .referenceNo(transaction.getCreditRef()) + .referenceDate(transaction.getCreatedAt().toLocalDate().toString()) + .postingDate(transaction.getCreatedAt().toLocalDate().toString()) + .references(Arrays.asList(paymentReferenceDto)) + .build(); + + try { + LinkedHashMap response = restService.postAs( + erpnextUrl + "/v2/document/Payment Entry", paymentEntryDto, headers, LinkedHashMap.class); + + LinkedHashMap data = (LinkedHashMap) response.get("data"); + String transactionId = (String) data.get("name"); + + transaction.setErpPurchasePaymentRef(transactionId); + transactionEvent.setStatus(Status.SUCCESS); + } catch (Exception e) { + e.printStackTrace(); + throw new ApiException("Failed to create purchase payment entry in ERPNext"); + } + + logger.info("purchase payment entry created successfully in ERPNext"); + } catch (Exception e) { + logger.error("Failed to create purchase payment entry in ERPNext: {}", e.getMessage(), e); + // Don't fail the transaction, just log the error + // The ERP invoice creation is a side effect, not critical to the payment flow + transactionEvent.setStatus(Status.FAILED); + transactionEvent.setMessage(e.getMessage()); + } + } else { + logger.info("Transaction status is not SUCCESS ({}), skipping purchase payment entry creation", transaction.getStatus()); + } + + transactionEventService.save(transactionEvent); + + return transactionService.save(transaction); + } +} diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/SalesInvoiceHandler.java b/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/SalesInvoiceHandler.java index e34fc48..c9e08f1 100644 --- a/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/SalesInvoiceHandler.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/SalesInvoiceHandler.java @@ -2,38 +2,146 @@ package zw.qantra.tm.domain.services.processors.handlers.erp; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; -import lombok.RequiredArgsConstructor; +import zw.qantra.tm.domain.dtos.erp.SalesInvoiceDto; +import zw.qantra.tm.domain.dtos.erp.SalesInvoiceItemDto; +import zw.qantra.tm.domain.enums.RequestType; import zw.qantra.tm.domain.enums.Status; import zw.qantra.tm.domain.models.Transaction; +import zw.qantra.tm.domain.models.TransactionEvent; +import zw.qantra.tm.domain.models.User; +import zw.qantra.tm.domain.services.TransactionEventService; +import zw.qantra.tm.domain.services.TransactionService; +import zw.qantra.tm.domain.services.UserService; import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; +import zw.qantra.tm.exceptions.ApiException; +import zw.qantra.tm.rest.RestService; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.Map; @Service -@RequiredArgsConstructor public class SalesInvoiceHandler implements LogicHandler{ Logger logger = LoggerFactory.getLogger(SalesInvoiceHandler.class); - + + @Autowired + private RestService restService; + @Autowired + private UserService userService; + @Autowired + private TransactionEventService transactionEventService; + @Autowired + private TransactionService transactionService; + + @Value("${erpnext.url}") + private String erpnextUrl; + @Value("${erpnext.api.key}") + private String erpnextApiKey; + @Value("${erpnext.api.secret}") + private String erpnextApiSecret; + @Value("${erpnext.username}") + private String erpnextUsername; + @Value("${erpnext.password}") + private String erpnextPassword; + @Value("${erpnext.company}") + private String erpnextCompany; + @Override public Transaction process(Transaction transaction) { logger.info("Processing sales invoice for transaction: {}", transaction.getReference()); - + TransactionEvent transactionEvent = TransactionEvent.builder() + .transactionId(transaction.getId().toString()) + .event(RequestType.SALES_INVOICE.name()) + .status(Status.PROCESSING) + .build(); + transactionEventService.save(transactionEvent); + if(transaction.getStatus() == Status.SUCCESS){ try { logger.info("Creating sales invoice in ERPNext for successful transaction"); - + // login to erp + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/json"); + headers.add("Accept", "application/json"); + + Map loginRequest = Map.of( + "usr", erpnextUsername, + "pwd", erpnextPassword); + + String sid = ""; + + try { + ResponseEntity response = restService.postAsEntity( + erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class); + + sid = response.getHeaders().getFirst("Set-Cookie"); + } catch (Exception e) { + e.printStackTrace(); + throw new ApiException("Failed to login to ERPNext"); + } + + // create customer in erp + String basicAuth = "Basic " + Base64.getEncoder().encodeToString( + (erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8)); + headers.add("Authorization", basicAuth); + headers.add("Cookie", sid); + + User user = userService.getUserRepository().findById(transaction.getUserId()).orElse(null); + if(user == null) + throw new ApiException("Could not find user"); + + SalesInvoiceDto salesInvoiceDto = SalesInvoiceDto.builder() + .doctype("Sales Invoice") + .customer(user.getErpCustomerRef()) + .postingDate(transaction.getCreatedAt().toLocalDate().toString()) + .dueDate(transaction.getCreatedAt().toLocalDate().toString()) + .company(erpnextCompany) + .docstatus(1) + .items(Arrays.asList(SalesInvoiceItemDto.builder() + .itemCode(transaction.getBillName()) + .qty(1) + .rate(transaction.getAmount().add(transaction.getGatewayCharge())) + .amount(transaction.getAmount().add(transaction.getGatewayCharge())) + .build())) + .build(); + + try { + LinkedHashMap response = restService.postAs( + erpnextUrl + "/v2/document/Sales Invoice", salesInvoiceDto, headers, LinkedHashMap.class); + + LinkedHashMap data = (LinkedHashMap) response.get("data"); + String transactionId = (String) data.get("name"); + + transaction.setErpSalesRef(transactionId); + transactionEvent.setStatus(Status.SUCCESS); + } catch (Exception e) { + e.printStackTrace(); + throw new ApiException("Failed to create sales invoice in ERPNext"); + } logger.info("Sales invoice created successfully in ERPNext"); } catch (Exception e) { logger.error("Failed to create sales invoice in ERPNext: {}", e.getMessage(), e); // Don't fail the transaction, just log the error // The ERP invoice creation is a side effect, not critical to the payment flow + transactionEvent.setStatus(Status.FAILED); + transactionEvent.setMessage(e.getMessage()); } } else { logger.info("Transaction status is not SUCCESS ({}), skipping sales invoice creation", transaction.getStatus()); } - - return transaction; + + transactionEventService.save(transactionEvent); + + return transactionService.save(transaction); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 27e0a82..8e61d8e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -34,9 +34,13 @@ erpnext.api.key=1102c6da0312f75 erpnext.api.secret=e3d95692bcf16c7 erpnext.username=Administrator erpnext.password=admin -erpnext.company=Qantra Payments -erpnext.item=Payment Service -erpnext.income.account=5111 - Sales Income - Q +erpnext.company=Qantra +erpnext.accounts.cash=Bank Account - Q +erpnext.accounts.gatewayexpense=Gateway Expenses - Q +erpnext.accounts.accountsreceivable=Debtors - Q +erpnext.accounts.commission=Discounts Received - Q +erpnext.accounts.payable=Creditors - Q +erpnext.income.account=Sales Income - Q erpnext.cost.center=Main - Q erpnext.tax.account=VAT - Q erpnext.tax.rate=15.0