package httpapi import ( "errors" "net/http" "strconv" "strings" "git.118im.com/im_team/im_workbench/internal/auth" "git.118im.com/im_team/im_workbench/internal/finance" "git.118im.com/im_team/im_workbench/internal/store" ) func (a *API) paymentAccounts(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet && r.Method != http.MethodPost { methodNotAllowed(w, r, http.MethodGet, http.MethodPost) return } principal, ok := a.authenticateFinanceMember(w, r) if !ok { return } if r.Method == http.MethodGet { result, err := a.finance.PaymentAccounts(r.Context(), principal.TenantID, principal.MemberID) if err != nil { writeFinanceError(w, r, err) return } writeSuccess(w, r, result) return } var body finance.PaymentAccountInput if !decodeBody(w, r, &body) { return } result, err := a.finance.CreatePaymentAccount(r.Context(), principal.TenantID, principal.MemberID, body) if err != nil { writeFinanceError(w, r, err) return } writeSuccess(w, r, result) } func (a *API) walletLedgers(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { methodNotAllowed(w, r, http.MethodGet) return } principal, ok := a.authenticateFinanceMember(w, r) if !ok { return } page, ok := optionalPositiveInt(w, r, "page") if !ok { writeFinanceError(w, r, finance.ErrPageInvalid) return } pageSize, ok := optionalPositiveInt(w, r, "page_size") if !ok { writeFinanceError(w, r, finance.ErrPageSizeInvalid) return } result, err := a.finance.WalletLedgers(r.Context(), principal.TenantID, principal.MemberID, page, pageSize, r.URL.Query().Get("biz_type"), r.URL.Query().Get("direction")) if err != nil { writeFinanceError(w, r, err) return } writeSuccess(w, r, result) } func (a *API) withdrawalQuote(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { methodNotAllowed(w, r, http.MethodPost) return } principal, ok := a.authenticateFinanceMember(w, r) if !ok { return } var body finance.QuoteInput if !decodeBody(w, r, &body) { return } result, err := a.finance.Quote(r.Context(), principal.TenantID, principal.MemberID, body) if err != nil { writeFinanceError(w, r, err) return } writeSuccess(w, r, result) } func (a *API) rechargeChannels(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { methodNotAllowed(w, r, http.MethodGet) return } principal, ok := a.authenticateFinanceMember(w, r) if !ok { return } result, err := a.finance.RechargeChannels(r.Context(), principal.TenantID) if err != nil { writeFinanceError(w, r, err) return } writeSuccess(w, r, result) } func (a *API) rechargeOrders(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet && r.Method != http.MethodPost { methodNotAllowed(w, r, http.MethodGet, http.MethodPost) return } principal, ok := a.authenticateFinanceMember(w, r) if !ok { return } if r.Method == http.MethodGet { page, ok := optionalPositiveInt(w, r, "page") if !ok { writeFinanceError(w, r, finance.ErrPageInvalid) return } pageSize, ok := optionalPositiveInt(w, r, "page_size") if !ok { writeFinanceError(w, r, finance.ErrPageSizeInvalid) return } result, err := a.finance.RechargeOrders(r.Context(), principal.TenantID, principal.MemberID, page, pageSize, r.URL.Query().Get("status")) if err != nil { writeFinanceError(w, r, err) return } writeSuccess(w, r, result) return } var body finance.CreateRechargeInput if !decodeBody(w, r, &body) { return } result, err := a.finance.CreateRecharge(r.Context(), principal.TenantID, principal.MemberID, body) if err != nil { writeFinanceError(w, r, err) return } writeSuccess(w, r, result) } func (a *API) rechargeOrder(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { methodNotAllowed(w, r, http.MethodGet) return } orderIDText := strings.Trim(strings.TrimPrefix(r.URL.Path, "/v1/recharge-orders/"), "/") if orderIDText == "" || strings.Contains(orderIDText, "/") { writeFinanceError(w, r, finance.ErrRechargeOrderIDInvalid) return } orderID, err := strconv.ParseInt(orderIDText, 10, 64) if err != nil || orderID <= 0 { writeFinanceError(w, r, finance.ErrRechargeOrderIDInvalid) return } principal, ok := a.authenticateFinanceMember(w, r) if !ok { return } result, err := a.finance.RechargeOrder(r.Context(), principal.TenantID, principal.MemberID, orderID) if err != nil { writeFinanceError(w, r, err) return } writeSuccess(w, r, result) } func (a *API) withdrawalOrders(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet && r.Method != http.MethodPost { methodNotAllowed(w, r, http.MethodGet, http.MethodPost) return } principal, ok := a.authenticateFinanceMember(w, r) if !ok { return } if r.Method == http.MethodGet { page, ok := optionalPositiveInt(w, r, "page") if !ok { writeFinanceError(w, r, finance.ErrPageInvalid) return } pageSize, ok := optionalPositiveInt(w, r, "page_size") if !ok { writeFinanceError(w, r, finance.ErrPageSizeInvalid) return } result, err := a.finance.WithdrawalOrders(r.Context(), principal.TenantID, principal.MemberID, page, pageSize, r.URL.Query().Get("status")) if err != nil { writeFinanceError(w, r, err) return } writeSuccess(w, r, result) return } var body finance.CreateWithdrawalInput if !decodeBody(w, r, &body) { return } result, err := a.finance.CreateWithdrawal(r.Context(), principal.TenantID, principal.MemberID, r.Header.Get("Idempotency-Key"), body) if err != nil { writeFinanceError(w, r, err) return } writeSuccess(w, r, result) } func (a *API) withdrawalOrder(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { methodNotAllowed(w, r, http.MethodGet) return } orderIDText := strings.Trim(strings.TrimPrefix(r.URL.Path, "/v1/withdrawal-orders/"), "/") if orderIDText == "" || strings.Contains(orderIDText, "/") { writeFinanceError(w, r, finance.ErrOrderIDInvalid) return } orderID, err := strconv.ParseInt(orderIDText, 10, 64) if err != nil || orderID <= 0 { writeFinanceError(w, r, finance.ErrOrderIDInvalid) return } principal, ok := a.authenticateFinanceMember(w, r) if !ok { return } result, err := a.finance.WithdrawalOrder(r.Context(), principal.TenantID, principal.MemberID, orderID) if err != nil { writeFinanceError(w, r, err) return } writeSuccess(w, r, result) } func (a *API) authenticateFinanceMember(w http.ResponseWriter, r *http.Request) (*auth.Principal, bool) { if a.finance == nil { writeError(w, r, http.StatusServiceUnavailable, 90001, "SERVICE_UNAVAILABLE", "service unavailable", nil) return nil, false } principal, ok := a.authenticateMember(w, r) if !ok { return nil, false } return principal, true } func writeFinanceError(w http.ResponseWriter, r *http.Request, err error) { switch { case errors.Is(err, finance.ErrAccountTypeInvalid): writeError(w, r, http.StatusBadRequest, 30000, "ACCOUNT_TYPE_INVALID", "validation failed", map[string]any{"field": "account_type"}) case errors.Is(err, finance.ErrAccountFieldsInvalid): writeError(w, r, http.StatusBadRequest, 30000, "PAYMENT_ACCOUNT_FIELDS_INVALID", "validation failed", nil) case errors.Is(err, finance.ErrTradePassword): writeError(w, r, http.StatusBadRequest, 30000, "TRADE_PASSWORD_INVALID", "trade password is invalid", map[string]any{"field": "trade_password"}) case errors.Is(err, finance.ErrTradePasswordUnset): writeError(w, r, http.StatusUnprocessableEntity, 30022, "TRADE_PASSWORD_NOT_SET", "trade password is not set", nil) case errors.Is(err, finance.ErrRateLimited): writeError(w, r, http.StatusTooManyRequests, 30020, "RATE_LIMITED", "too many trade password attempts", map[string]any{"retry_after_seconds": 900}) case errors.Is(err, finance.ErrCurrencyInvalid): writeError(w, r, http.StatusBadRequest, 30000, "CURRENCY_INVALID", "validation failed", map[string]any{"field": "currency"}) case errors.Is(err, finance.ErrAmountInvalid), errors.Is(err, store.ErrWithdrawalAmount): writeError(w, r, http.StatusUnprocessableEntity, 30022, "WITHDRAWAL_AMOUNT_INVALID", "withdrawal amount is outside the allowed range", nil) case errors.Is(err, store.ErrRechargeAmount): writeError(w, r, http.StatusUnprocessableEntity, 30022, "RECHARGE_AMOUNT_INVALID", "recharge amount is outside the allowed range", nil) case errors.Is(err, finance.ErrRemarkInvalid): writeError(w, r, http.StatusBadRequest, 30000, "RECHARGE_REMARK_INVALID", "validation failed", map[string]any{"field": "remark"}) case errors.Is(err, finance.ErrVoucherInvalid): writeError(w, r, http.StatusBadRequest, 30000, "RECHARGE_VOUCHER_INVALID", "validation failed", map[string]any{"field": "voucher_attachment_ids"}) case errors.Is(err, finance.ErrIdempotencyKey): writeError(w, r, http.StatusBadRequest, 30000, "IDEMPOTENCY_KEY_INVALID", "valid Idempotency-Key header is required", map[string]any{"field": "Idempotency-Key"}) case errors.Is(err, finance.ErrPageInvalid): writeError(w, r, http.StatusBadRequest, 30000, "PAGE_INVALID", "validation failed", map[string]any{"field": "page"}) case errors.Is(err, finance.ErrPageSizeInvalid): writeError(w, r, http.StatusBadRequest, 30000, "PAGE_SIZE_INVALID", "validation failed", map[string]any{"field": "page_size"}) case errors.Is(err, finance.ErrLedgerFilterInvalid): writeError(w, r, http.StatusBadRequest, 30000, "LEDGER_FILTER_INVALID", "validation failed", nil) case errors.Is(err, finance.ErrStatusInvalid): writeError(w, r, http.StatusBadRequest, 30000, "WITHDRAWAL_STATUS_INVALID", "validation failed", map[string]any{"field": "status"}) case errors.Is(err, finance.ErrRechargeStatusInvalid): writeError(w, r, http.StatusBadRequest, 30000, "RECHARGE_STATUS_INVALID", "validation failed", map[string]any{"field": "status"}) case errors.Is(err, finance.ErrRechargeOrderIDInvalid): writeError(w, r, http.StatusBadRequest, 30000, "RECHARGE_ORDER_ID_INVALID", "validation failed", map[string]any{"field": "order_id"}) case errors.Is(err, finance.ErrOrderIDInvalid): writeError(w, r, http.StatusBadRequest, 30000, "WITHDRAWAL_ORDER_ID_INVALID", "validation failed", map[string]any{"field": "order_id"}) case errors.Is(err, store.ErrPaymentAccountExists): writeError(w, r, http.StatusConflict, 30040, "PAYMENT_ACCOUNT_EXISTS", "payment account already exists", nil) case errors.Is(err, store.ErrPaymentAccountInvalid): writeError(w, r, http.StatusUnprocessableEntity, 30022, "PAYMENT_ACCOUNT_INVALID", "payment account is unavailable or unverified", nil) case errors.Is(err, store.ErrWithdrawalDisabled): writeError(w, r, http.StatusUnprocessableEntity, 30022, "WITHDRAWAL_DISABLED", "withdrawal feature is disabled", nil) case errors.Is(err, store.ErrWithdrawalChannel): writeError(w, r, http.StatusServiceUnavailable, 30053, "WITHDRAWAL_CHANNEL_UNAVAILABLE", "withdrawal channel is unavailable", nil) case errors.Is(err, store.ErrRechargeChannel): writeError(w, r, http.StatusServiceUnavailable, 30053, "RECHARGE_CHANNEL_UNAVAILABLE", "recharge channel is unavailable", nil) case errors.Is(err, store.ErrWithdrawalLimit): writeError(w, r, http.StatusUnprocessableEntity, 30022, "WITHDRAWAL_DAILY_LIMIT_REACHED", "daily withdrawal limit reached", nil) case errors.Is(err, store.ErrInsufficientBalance): writeError(w, r, http.StatusUnprocessableEntity, 30022, "INSUFFICIENT_BALANCE", "insufficient available balance", nil) case errors.Is(err, store.ErrAccountRestricted): writeError(w, r, http.StatusUnprocessableEntity, 30022, "ACCOUNT_RESTRICTED", "account is restricted", nil) case errors.Is(err, store.ErrAccountNotFound): writeError(w, r, http.StatusUnprocessableEntity, 30022, "ACCOUNT_RESTRICTED", "account is restricted", nil) case errors.Is(err, store.ErrWithdrawalNotFound): writeError(w, r, http.StatusNotFound, 30044, "WITHDRAWAL_ORDER_NOT_FOUND", "withdrawal order not found", nil) case errors.Is(err, store.ErrRechargeNotFound): writeError(w, r, http.StatusNotFound, 30044, "RECHARGE_ORDER_NOT_FOUND", "recharge order not found", nil) case errors.Is(err, store.ErrRechargeVoucher): writeError(w, r, http.StatusBadRequest, 30000, "RECHARGE_VOUCHER_INVALID", "validation failed", map[string]any{"field": "voucher_attachment_ids"}) case errors.Is(err, store.ErrIdempotencyConflict): writeError(w, r, http.StatusConflict, 30040, "IDEMPOTENCY_CONFLICT", "Idempotency-Key was reused with different request data", nil) case errors.Is(err, store.ErrAttachmentInvalid): writeError(w, r, http.StatusBadRequest, 30000, "QR_ATTACHMENT_INVALID", "validation failed", map[string]any{"field": "qr_attachment_id"}) case errors.Is(err, store.ErrConcurrentUpdate): writeError(w, r, http.StatusConflict, 30040, "ACCOUNT_CHANGED", "account changed, retry the operation", nil) case errors.Is(err, finance.ErrUnavailable): writeError(w, r, http.StatusServiceUnavailable, 90001, "SERVICE_UNAVAILABLE", "service unavailable", nil) default: writeError(w, r, http.StatusInternalServerError, 90000, reasonInternal, "internal error", nil) } }