diff --git a/internal/entity/linkqu.go b/internal/entity/linkqu.go index acf4d74..009b64f 100644 --- a/internal/entity/linkqu.go +++ b/internal/entity/linkqu.go @@ -12,6 +12,13 @@ type LinkQuRequest struct { OrderItems []OrderItem } +type LinkQuCallback struct { + PartnerReff string + PaymentReff string + Status string + Signature string +} + type LinkQuQRISResponse struct { Time int `json:"time"` Amount int64 `json:"amount"` @@ -56,3 +63,36 @@ type LinkQuPaymentVAResponse struct { Signature string `json:"signature"` UrlCallback string `json:"url_callback"` } + +type LinkQuCheckStatusResponse struct { + ResponseCode string `json:"rc"` + ResponseDesc string `json:"rd"` + Total int64 `json:"total"` + Balance int64 `json:"balance"` + Data LinkQuCheckStatusData `json:"data"` + Request map[string]interface{} `json:"request"` + LastUpdate string `json:"lastUpdate"` + DataAdditional map[string]interface{} `json:"dataadditional"` +} + +type LinkQuCheckStatusData struct { + InquiryReff int64 `json:"inquiry_reff"` + PaymentReff int64 `json:"payment_reff"` + PartnerReff string `json:"partner_reff"` + Reference string `json:"reference"` + ProductID string `json:"id_produk"` + ProductName string `json:"nama_produk"` + ProductGroup string `json:"grup_produk"` + Debitted int64 `json:"debitted"` + Amount int64 `json:"amount"` + AmountFee int64 `json:"amountfee"` + Info1 string `json:"info1"` + Info2 string `json:"info2"` + Info3 string `json:"info3"` + Info4 string `json:"info4"` + StatusTrx string `json:"status_trx"` + StatusDesc string `json:"status_desc"` + StatusPaid string `json:"status_paid"` + Balance int64 `json:"balance"` + TipsQRIS int64 `json:"tips_qris"` +} diff --git a/internal/handlers/http/customerorder/order.go b/internal/handlers/http/customerorder/order.go index 7cb666e..0259314 100644 --- a/internal/handlers/http/customerorder/order.go +++ b/internal/handlers/http/customerorder/order.go @@ -7,6 +7,7 @@ import ( "furtuna-be/internal/handlers/request" "furtuna-be/internal/handlers/response" "furtuna-be/internal/services" + "furtuna-be/internal/utils" "net/http" "time" @@ -56,7 +57,7 @@ func (h *Handler) Inquiry(c *gin.Context) { c.JSON(http.StatusOK, response.BaseResponse{ Success: true, Status: http.StatusOK, - Data: MapOrderToCreateOrderResponse(order), + Data: MapOrderToCreateOrderResponse(order, req), }) } @@ -88,7 +89,7 @@ func (h *Handler) Execute(c *gin.Context) { }) } -func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse) response.CreateOrderResponse { +func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse, req request.CustomerOrder) response.CreateOrderResponse { order := orderResponse.Order orderItems := make([]response.CreateOrderItemResponse, len(order.OrderItems)) for i, item := range order.OrderItems { @@ -115,6 +116,8 @@ func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse) response Total: order.Total, VisitDate: order.VisitDate.Format("2006-01-02"), SiteName: order.Site.Name, + BankCode: req.BankCode, + BankName: utils.BankName(req.BankCode), } } diff --git a/internal/handlers/http/linqu/order.go b/internal/handlers/http/linqu/order.go index ae31f44..02fe41d 100644 --- a/internal/handlers/http/linqu/order.go +++ b/internal/handlers/http/linqu/order.go @@ -1,6 +1,9 @@ -package mdtrns +package linkqu import ( + "encoding/json" + "fmt" + "furtuna-be/internal/entity" "furtuna-be/internal/handlers/request" "furtuna-be/internal/handlers/response" "furtuna-be/internal/services" @@ -13,7 +16,7 @@ type Handler struct { } func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/midtrans") + route := group.Group("/linkqu") route.POST("/callback", h.Callback) } @@ -25,35 +28,24 @@ func NewHandler(service services.Order) *Handler { } func (h *Handler) Callback(c *gin.Context) { - var callbackData request.MidtransCallbackRequest + var callbackData request.LinQuCallback if err := c.ShouldBindJSON(&callbackData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - validStatuses := []string{"settlement", "expire", "deny", "cancel", "capture", "failure"} + data, _ := json.Marshal(callbackData) + fmt.Println(string(data)) - isValidStatus := false - for _, status := range validStatuses { - if callbackData.TransactionStatus == status { - isValidStatus = true - break - } - } - - if !isValidStatus { - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Message: "", - }) - return - } - - err := h.service.ProcessCallback(c, callbackData.ToEntity()) + err := h.service.ProcessLinkQuCallback(c, &entity.LinkQuCallback{ + PaymentReff: callbackData.PartnerReff2, + Status: callbackData.Status, + Signature: callbackData.Signature, + PartnerReff: callbackData.PartnerReff, + }) if err != nil { - c.JSON(http.StatusUnauthorized, response.BaseResponse{ + c.JSON(http.StatusBadRequest, response.BaseResponse{ Success: false, Status: http.StatusBadRequest, Message: err.Error(), @@ -63,9 +55,10 @@ func (h *Handler) Callback(c *gin.Context) { } c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Message: "order", + Success: true, + Status: http.StatusOK, + Message: "order", + Response: "00", }) } diff --git a/internal/handlers/request/midtrans.go b/internal/handlers/request/midtrans.go index f73a865..445e1cc 100644 --- a/internal/handlers/request/midtrans.go +++ b/internal/handlers/request/midtrans.go @@ -37,3 +37,22 @@ func (m *MidtransCallbackRequest) ToEntity() *entity.CallbackRequest { TransactionStatus: m.TransactionStatus, } } + +type LinQuCallback struct { + Amount int `json:"amount"` + SerialNumber string `json:"serialnumber"` + Type string `json:"type"` + PaymentReff int64 `json:"payment_reff"` + VaCode string `json:"va_code"` + PartnerReff string `json:"partner_reff"` + PartnerReff2 string `json:"partner_reff2"` + AdditionalFee int `json:"additionalfee"` + Balance int `json:"balance"` + CreditBalance int `json:"credit_balance"` + TransactionTime string `json:"transaction_time"` + VaNumber string `json:"va_number"` + CustomerName string `json:"customer_name"` + Username string `json:"username"` + Status string `json:"status"` + Signature string `json:"signature"` +} diff --git a/internal/handlers/response/base_response.go b/internal/handlers/response/base_response.go index 6bfb27b..baeeb28 100644 --- a/internal/handlers/response/base_response.go +++ b/internal/handlers/response/base_response.go @@ -9,4 +9,5 @@ type BaseResponse struct { ErrorDetail interface{} `json:"error_detail,omitempty"` Data interface{} `json:"data,omitempty"` PagingMeta *PagingMeta `json:"meta,omitempty"` + Response string `json:"response,omitempty"` } diff --git a/internal/handlers/response/order.go b/internal/handlers/response/order.go index e62400f..53f2ab8 100644 --- a/internal/handlers/response/order.go +++ b/internal/handlers/response/order.go @@ -96,6 +96,8 @@ type CreateOrderResponse struct { CreatedAt time.Time `json:"created_at"` OrderItems []CreateOrderItemResponse `json:"order_items"` Token string `json:"token"` + BankCode string `json:"bank_code"` + BankName string `json:"bank_name"` } type PrintDetailResponse struct { diff --git a/internal/repository/linkqu/linkqu.go b/internal/repository/linkqu/linkqu.go index b1f4a47..020a40b 100644 --- a/internal/repository/linkqu/linkqu.go +++ b/internal/repository/linkqu/linkqu.go @@ -227,3 +227,47 @@ func cleanString(s string) string { reg := regexp.MustCompile("[^a-zA-Z0-9]+") return strings.ToLower(reg.ReplaceAllString(s, "")) } + +func (s *LinkQuService) CheckPaymentStatus(partnerReff string) (*entity.LinkQuCheckStatusResponse, error) { + path := "/transaction/payment/checkstatus" + method := "GET" + + url := fmt.Sprintf("%s%s%s?username=%s&partnerreff=%s", + s.config.LinkQuBaseURL(), "/linkqu-partner", path, s.config.LinkQuUsername(), partnerReff) + + httpReq, err := http.NewRequest(method, url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create HTTP request: %w", err) + } + + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("client-id", s.config.LinkQuClientID()) + httpReq.Header.Set("client-secret", s.config.LinkQuClientSecret()) + + resp, err := s.client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) + } + + // Parse response + var checkStatusResp entity.LinkQuCheckStatusResponse + if err := json.Unmarshal(body, &checkStatusResp); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + if checkStatusResp.ResponseCode != "00" { + return nil, fmt.Errorf("error when checking payment status, status code %s", checkStatusResp.ResponseCode) + } + + return &checkStatusResp, nil +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 397be6a..bd3d234 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -6,6 +6,7 @@ import ( "furtuna-be/internal/common/mycontext" "furtuna-be/internal/repository/brevo" "furtuna-be/internal/repository/license" + "furtuna-be/internal/repository/linkqu" mdtrns "furtuna-be/internal/repository/midtrans" "furtuna-be/internal/repository/orders" "furtuna-be/internal/repository/oss" @@ -49,6 +50,7 @@ type RepoManagerImpl struct { License License Transaction TransactionRepository PG PaymentGateway + LinkQu LinkQu } func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl { @@ -71,6 +73,7 @@ func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl { License: license.NewLicenseRepository(db), Transaction: transactions.NewTransactionRepository(db), PG: pg.NewPaymentGatewayRepo(&cfg.Midtrans, &cfg.LinkQu), + LinkQu: linkqu.NewLinkQuService(&cfg.LinkQu), } } @@ -218,9 +221,10 @@ type TransactionRepository interface { Update(ctx context.Context, trx *gorm.DB, transaction *entity.Transaction) (*entity.Transaction, error) } -type LinQu interface { +type LinkQu interface { CreateQrisPayment(linkQuRequest entity.LinkQuRequest) (*entity.LinkQuQRISResponse, error) CreatePaymentVA(linkQuRequest entity.LinkQuRequest) (*entity.LinkQuPaymentVAResponse, error) + CheckPaymentStatus(partnerReff string) (*entity.LinkQuCheckStatusResponse, error) } type PaymentGateway interface { diff --git a/internal/routes/routes.go b/internal/routes/routes.go index 0190744..c960a87 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -3,6 +3,7 @@ package routes import ( "furtuna-be/internal/handlers/http/balance" "furtuna-be/internal/handlers/http/license" + linkqu "furtuna-be/internal/handlers/http/linqu" mdtrns "furtuna-be/internal/handlers/http/midtrans" "furtuna-be/internal/handlers/http/order" "furtuna-be/internal/handlers/http/oss" @@ -60,6 +61,7 @@ func RegisterPrivateRoutes(app *app.Server, serviceManager *services.ServiceMana license.NewHandler(serviceManager.LicenseSvc), transaction.New(serviceManager.Transaction), balance.NewHandler(serviceManager.Balance), + linkqu.NewHandler(serviceManager.OrderSvc), } for _, handler := range serverRoutes { diff --git a/internal/services/auth/init.go b/internal/services/auth/init.go index 5ba042d..1b9856b 100644 --- a/internal/services/auth/init.go +++ b/internal/services/auth/init.go @@ -51,9 +51,9 @@ func (u *AuthServiceImpl) AuthenticateUser(ctx context.Context, email, password return nil, errors.ErrorUserIsNotFound } - //if ok := u.crypto.CompareHashAndPassword(user.Password, password); !ok { - // //return nil, errors.ErrorUserInvalidLogin - //} + if ok := u.crypto.CompareHashAndPassword(user.Password, password); !ok { + return nil, errors.ErrorUserInvalidLogin + } signedToken, err := u.crypto.GenerateJWT(user.ToUser()) diff --git a/internal/services/order/order.go b/internal/services/order/order.go index 2ec5fd2..3dabeb2 100644 --- a/internal/services/order/order.go +++ b/internal/services/order/order.go @@ -32,6 +32,7 @@ type OrderService struct { transaction repository.TransactionRepository txmanager repository.TransactionManager wallet repository.WalletRepository + linkquRepo repository.LinkQu cfg Config } @@ -42,6 +43,7 @@ func NewOrderService( txmanager repository.TransactionManager, wallet repository.WalletRepository, cfg Config, transaction repository.TransactionRepository, + linkquRepo repository.LinkQu, ) *OrderService { return &OrderService{ repo: repo, @@ -53,6 +55,7 @@ func NewOrderService( wallet: wallet, cfg: cfg, transaction: transaction, + linkquRepo: linkquRepo, } } @@ -558,9 +561,9 @@ func (s *OrderService) processPayment(ctx context.Context, tx *gorm.DB, req *ent func updatePaymentState(status string) string { switch status { - case "settlement", "capture": + case "settlement", "capture", "paid", "settle": return "PAID" - case "expire", "deny", "cancel", "failure": + case "expire", "deny", "cancel", "failure", "EXPIRED": return "EXPIRED" default: return status @@ -680,3 +683,31 @@ func (s *OrderService) GetPrintDetail(ctx mycontext.Context, id int64) (*entity. return order, nil } + +func (s *OrderService) ProcessLinkQuCallback(ctx context.Context, req *entity.LinkQuCallback) error { + tx, err := s.txmanager.Begin(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable}) + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + defer tx.Rollback() + + pay, err := s.linkquRepo.CheckPaymentStatus(req.PaymentReff) + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + + if pay.ResponseCode != "00" { + return nil + } + + err = s.processPayment(ctx, tx, &entity.CallbackRequest{ + TransactionID: req.PartnerReff, + TransactionStatus: pay.Data.StatusPaid, + }) + + if err != nil { + return fmt.Errorf("failed to process payment: %w", err) + } + + return tx.Commit().Error +} diff --git a/internal/services/service.go b/internal/services/service.go index dac6f59..47ee8f6 100644 --- a/internal/services/service.go +++ b/internal/services/service.go @@ -47,7 +47,7 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) UserSvc: users.NewUserService(repo.User), StudioSvc: studio.NewStudioService(repo.Studio), ProductSvc: product.NewProductService(repo.Product), - OrderSvc: order.NewOrderService(repo.Order, repo.Product, repo.Crypto, repo.PG, repo.Payment, repo.Trx, repo.Wallet, &cfg.Order, repo.Transaction), + OrderSvc: order.NewOrderService(repo.Order, repo.Product, repo.Crypto, repo.PG, repo.Payment, repo.Trx, repo.Wallet, &cfg.Order, repo.Transaction, repo.LinkQu), OSSSvc: oss.NewOSSService(repo.OSS), PartnerSvc: partner.NewPartnerService( repo.Partner, users.NewUserService(repo.User), repo.Trx, repo.Wallet, repo.User), @@ -114,6 +114,7 @@ type Order interface { GetPaymentDistribution(ctx mycontext.Context, req entity.OrderSearch) ([]entity.PaymentTypeDistribution, error) GetByID(ctx mycontext.Context, id int64, referenceID string) (*entity.Order, error) GetPrintDetail(ctx mycontext.Context, id int64) (*entity.OrderPrintDetail, error) + ProcessLinkQuCallback(ctx context.Context, req *entity.LinkQuCallback) error } type OSSService interface { diff --git a/internal/utils/bank_code.go b/internal/utils/bank_code.go index d4b585b..b36a0e2 100644 --- a/internal/utils/bank_code.go +++ b/internal/utils/bank_code.go @@ -1 +1,16 @@ package utils + +func BankName(bankCode string) string { + bankCodeMap := map[string]string{ + "001": "Bank Central Asia (BCA)", + "002": "Bank Rakyat Indonesia (BRI)", + "008": "Bank Mandiri", + "009": "Bank Negara Indonesia (BNI)", + "014": "Bank Tabungan Negara (BTN)", + } + + if bankName, exists := bankCodeMap[bankCode]; exists { + return bankName + } + return "Bank code not recognized" +}