351 lines
13 KiB
Go
351 lines
13 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.118im.com/im_team/olons_server/internal/auth"
|
|
"git.118im.com/im_team/olons_server/internal/finance"
|
|
"git.118im.com/im_team/olons_server/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) paymentAccountDetails(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
|
|
}
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
result, err := a.finance.PaymentAccountDetails(r.Context(), principal.TenantID, principal.MemberID)
|
|
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)
|
|
}
|
|
}
|