Files
olons_server/internal/httpapi/finance.go
T
2026-10-12 23:26:36 +08:00

303 lines
12 KiB
Go

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) 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.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)
}
}