534 lines
20 KiB
Go
534 lines
20 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/payments"
|
|
)
|
|
|
|
const refundColumns = `r.id, r.order_id, r.organization_id, r.operator_id,
|
|
COALESCE(u.display_name, ''), r.request_key, r.mch_id, r.transaction_id,
|
|
COALESCE(r.provider_id, ''), r.amount_fen, r.total_fen, r.currency, r.reason,
|
|
r.status, r.failure_reason, r.created_at, r.updated_at, r.succeeded_at`
|
|
|
|
const refundSelect = `SELECT ` + refundColumns + ` FROM public.payment_refunds r
|
|
LEFT JOIN public.platform_users u ON u.id = r.operator_id `
|
|
|
|
const adminOrdersSQL = `SELECT payment_orders.id, payment_orders.organization_id,
|
|
payment_orders.account_id, payment_orders.request_key, payment_orders.mch_id,
|
|
payment_orders.app_id, payment_orders.amount_fen, payment_orders.currency,
|
|
payment_orders.status, COALESCE(payment_orders.code_url, ''),
|
|
COALESCE(payment_orders.transaction_id, ''), payment_orders.created_at,
|
|
payment_orders.updated_at, payment_orders.expires_at, payment_orders.paid_at,
|
|
COALESCE(org.name, ''), COALESCE(account.display_name, ''),
|
|
COALESCE(refunds.refunded_fen, 0), COALESCE(refunds.refunding_fen, 0)
|
|
FROM public.payment_orders
|
|
JOIN public.platform_organizations org ON org.id = payment_orders.organization_id
|
|
JOIN public.platform_users account ON account.id = payment_orders.account_id
|
|
LEFT JOIN LATERAL (
|
|
SELECT sum(amount_fen) FILTER (WHERE status = 'success') AS refunded_fen,
|
|
sum(amount_fen) FILTER (WHERE status IN ('pending', 'processing', 'abnormal')) AS refunding_fen
|
|
FROM public.payment_refunds WHERE order_id = payment_orders.id
|
|
) refunds ON true
|
|
WHERE ($1::text = '' OR payment_orders.organization_id = $1::text)
|
|
ORDER BY payment_orders.created_at DESC, payment_orders.id DESC
|
|
LIMIT $2::integer OFFSET $3::integer`
|
|
|
|
// The caller requests limit+1 to discover whether another page exists.
|
|
func (db *Database) ListAdminPaymentOrders(ctx context.Context, organizationID string, limit, offset int) ([]payments.AdminOrder, error) {
|
|
q, err := db.paymentQuerier()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if offset < 0 {
|
|
return nil, payments.ErrConflict
|
|
}
|
|
rows, err := q.Query(ctx, adminOrdersSQL, organizationID, boundedAdminOrdersLimit(limit), offset)
|
|
if err != nil {
|
|
return nil, paymentSQLError("list admin payment orders", err)
|
|
}
|
|
defer rows.Close()
|
|
out := make([]payments.AdminOrder, 0)
|
|
for rows.Next() {
|
|
var item payments.AdminOrder
|
|
var paidAt sql.NullTime
|
|
if err := rows.Scan(&item.ID, &item.OrganizationID, &item.AccountID, &item.RequestKey,
|
|
&item.MchID, &item.AppID, &item.AmountFen, &item.Currency, &item.Status,
|
|
&item.CodeURL, &item.TransactionID, &item.CreatedAt, &item.UpdatedAt,
|
|
&item.ExpiresAt, &paidAt, &item.OrganizationName, &item.AccountName,
|
|
&item.RefundedFen, &item.RefundingFen); err != nil {
|
|
return nil, paymentSQLError("scan admin payment order", err)
|
|
}
|
|
if paidAt.Valid {
|
|
item.PaidAt = &paidAt.Time
|
|
}
|
|
item.CodeURL = ""
|
|
item.RefundableFen = item.AmountFen - item.RefundedFen - item.RefundingFen
|
|
if item.Status != payments.StatusPaid || item.RefundableFen < 0 {
|
|
item.RefundableFen = 0
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
return out, paymentSQLError("read admin payment orders", rows.Err())
|
|
}
|
|
|
|
func boundedAdminOrdersLimit(limit int) int {
|
|
if limit < 1 {
|
|
return 1
|
|
}
|
|
if limit > 101 {
|
|
return 101
|
|
}
|
|
return limit
|
|
}
|
|
|
|
func (db *Database) CreatePaymentRefund(ctx context.Context, input payments.Refund) (payments.Refund, error) {
|
|
if _, err := db.paymentQuerier(); err != nil || db.transactions == nil {
|
|
return payments.Refund{}, payments.ErrUnavailable
|
|
}
|
|
if !validPaymentKey(input.ID, 32) || !validPaymentKey(input.RequestKey, 64) ||
|
|
!validPaymentKey(input.OrderID, 32) || input.OperatorID == "" || input.AmountFen <= 0 ||
|
|
len(input.Reason) > 80 {
|
|
return payments.Refund{}, payments.ErrConflict
|
|
}
|
|
tx, err := db.transactions.Begin(ctx)
|
|
if err != nil {
|
|
return payments.Refund{}, paymentSQLError("begin refund reservation", err)
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
order, found, err := queryPaymentOrder(ctx, tx, lockPaymentOrderSQL, input.OrderID)
|
|
if err != nil {
|
|
return payments.Refund{}, paymentSQLError("lock original payment", err)
|
|
}
|
|
if !found {
|
|
return payments.Refund{}, payments.ErrNotFound
|
|
}
|
|
if order.Status != payments.StatusPaid {
|
|
return payments.Refund{}, payments.ErrConflict
|
|
}
|
|
operatorOK, err := queryRefundOperator(ctx, tx, input.OperatorID)
|
|
if err != nil {
|
|
return payments.Refund{}, paymentSQLError("verify refund operator", err)
|
|
}
|
|
if !operatorOK {
|
|
return payments.Refund{}, payments.ErrConflict
|
|
}
|
|
previous, found, err := queryRefund(ctx, tx, refundSelect+` WHERE r.order_id = $1 AND r.request_key = $2`, order.ID, input.RequestKey)
|
|
if err != nil {
|
|
return payments.Refund{}, paymentSQLError("read refund request", err)
|
|
}
|
|
if found {
|
|
if previous.OperatorID != input.OperatorID || previous.AmountFen != input.AmountFen || previous.Reason != input.Reason {
|
|
return payments.Refund{}, payments.ErrConflict
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return payments.Refund{}, paymentSQLError("commit refund replay", err)
|
|
}
|
|
return previous, nil
|
|
}
|
|
used, err := queryRefundUsed(ctx, tx, order.ID, "")
|
|
if err != nil {
|
|
return payments.Refund{}, paymentSQLError("check refundable balance", err)
|
|
}
|
|
if used > order.AmountFen || input.AmountFen > order.AmountFen-used {
|
|
return payments.Refund{}, payments.ErrInvalidAmount
|
|
}
|
|
created, found, err := queryRefund(ctx, tx, `INSERT INTO public.payment_refunds
|
|
(id, order_id, organization_id, operator_id, request_key, mch_id, transaction_id,
|
|
amount_fen, total_fen, currency, reason)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
RETURNING `+refundColumnsNoJoin,
|
|
input.ID, order.ID, order.OrganizationID, input.OperatorID, input.RequestKey,
|
|
order.MchID, order.TransactionID, input.AmountFen, order.AmountFen, order.Currency, input.Reason)
|
|
if err != nil {
|
|
return payments.Refund{}, paymentSQLError("create refund reservation", err)
|
|
}
|
|
if !found {
|
|
return payments.Refund{}, payments.ErrConflict
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return payments.Refund{}, paymentSQLError("commit refund reservation", err)
|
|
}
|
|
return created, nil
|
|
}
|
|
|
|
const refundColumnsNoJoin = `id, order_id, organization_id, operator_id, ''::text, request_key,
|
|
mch_id, transaction_id, COALESCE(provider_id, ''), amount_fen, total_fen,
|
|
currency, reason, status, failure_reason, created_at, updated_at, succeeded_at`
|
|
|
|
func queryRefundOperator(ctx context.Context, q Querier, operatorID string) (bool, error) {
|
|
rows, err := q.Query(ctx, `SELECT 1 FROM public.platform_users WHERE id = $1 AND role = 'super_admin' AND status = 'active'`, operatorID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer rows.Close()
|
|
if !rows.Next() {
|
|
return false, rows.Err()
|
|
}
|
|
var one int
|
|
if err := rows.Scan(&one); err != nil {
|
|
return false, err
|
|
}
|
|
return one == 1, rows.Err()
|
|
}
|
|
|
|
func queryRefundUsed(ctx context.Context, q Querier, orderID, excludeID string) (int64, error) {
|
|
rows, err := q.Query(ctx, `SELECT COALESCE(sum(amount_fen), 0)::bigint FROM public.payment_refunds
|
|
WHERE order_id = $1 AND id <> $2 AND status IN ('pending', 'processing', 'abnormal', 'success')`, orderID, excludeID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer rows.Close()
|
|
if !rows.Next() {
|
|
return 0, rows.Err()
|
|
}
|
|
var used int64
|
|
if err := rows.Scan(&used); err != nil {
|
|
return 0, err
|
|
}
|
|
return used, rows.Err()
|
|
}
|
|
|
|
func (db *Database) GetPaymentRefund(ctx context.Context, id string) (payments.Refund, error) {
|
|
q, err := db.paymentQuerier()
|
|
if err != nil {
|
|
return payments.Refund{}, err
|
|
}
|
|
item, found, err := queryRefund(ctx, q, refundSelect+` WHERE r.id = $1`, id)
|
|
if err != nil {
|
|
return payments.Refund{}, paymentSQLError("get refund", err)
|
|
}
|
|
if !found {
|
|
return payments.Refund{}, payments.ErrNotFound
|
|
}
|
|
return item, nil
|
|
}
|
|
|
|
func (db *Database) ListPaymentRefunds(ctx context.Context, orderID string) ([]payments.Refund, error) {
|
|
q, err := db.paymentQuerier()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return queryRefunds(ctx, q, refundSelect+` WHERE r.order_id = $1 ORDER BY r.created_at DESC, r.id DESC`, orderID)
|
|
}
|
|
|
|
func (db *Database) PendingPaymentRefunds(ctx context.Context, limit int) ([]payments.Refund, error) {
|
|
q, err := db.paymentQuerier()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return queryRefunds(ctx, q, refundSelect+` WHERE r.status IN ('pending', 'processing', 'abnormal')
|
|
ORDER BY r.updated_at, r.created_at, r.id LIMIT $1`, boundedPaymentLimit(limit))
|
|
}
|
|
|
|
func (db *Database) TouchPaymentRefund(ctx context.Context, id string) (payments.Refund, error) {
|
|
q, err := db.paymentQuerier()
|
|
if err != nil {
|
|
return payments.Refund{}, err
|
|
}
|
|
item, found, err := queryRefund(ctx, q, `UPDATE public.payment_refunds
|
|
SET updated_at = now() WHERE id = $1 AND status IN ('pending', 'processing', 'abnormal')
|
|
RETURNING `+refundColumnsNoJoin, id)
|
|
if err != nil {
|
|
return payments.Refund{}, paymentSQLError("touch refund", err)
|
|
}
|
|
if found {
|
|
return item, nil
|
|
}
|
|
return db.GetPaymentRefund(ctx, id)
|
|
}
|
|
|
|
// Reject only an unsubmitted request. The original payment lock serializes
|
|
// reservation release with new requests and late provider notifications.
|
|
func (db *Database) RejectPaymentRefund(ctx context.Context, id, reason string) (payments.Refund, error) {
|
|
if _, err := db.paymentQuerier(); err != nil || db.transactions == nil {
|
|
return payments.Refund{}, payments.ErrUnavailable
|
|
}
|
|
if !validPaymentKey(id, 32) || reason == "" || len(reason) > 160 || !utf8.ValidString(reason) ||
|
|
strings.IndexFunc(reason, unicode.IsControl) >= 0 {
|
|
return payments.Refund{}, payments.ErrConflict
|
|
}
|
|
tx, err := db.transactions.Begin(ctx)
|
|
if err != nil {
|
|
return payments.Refund{}, paymentSQLError("begin refund rejection", err)
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
initial, found, err := queryRefund(ctx, tx, refundSelect+` WHERE r.id = $1`, id)
|
|
if err != nil {
|
|
return payments.Refund{}, paymentSQLError("read refund for rejection", err)
|
|
}
|
|
if !found {
|
|
return payments.Refund{}, payments.ErrNotFound
|
|
}
|
|
if _, found, err := queryPaymentOrder(ctx, tx, lockPaymentOrderSQL, initial.OrderID); err != nil {
|
|
return payments.Refund{}, paymentSQLError("lock original payment for rejection", err)
|
|
} else if !found {
|
|
return payments.Refund{}, payments.ErrNotFound
|
|
}
|
|
item, found, err := queryRefund(ctx, tx, refundSelect+` WHERE r.id = $1 FOR UPDATE OF r`, id)
|
|
if err != nil {
|
|
return payments.Refund{}, paymentSQLError("lock refund for rejection", err)
|
|
}
|
|
if !found {
|
|
return payments.Refund{}, payments.ErrNotFound
|
|
}
|
|
if item.OrderID != initial.OrderID {
|
|
return payments.Refund{}, payments.ErrConflict
|
|
}
|
|
if item.Status == payments.RefundRejected {
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return payments.Refund{}, paymentSQLError("commit rejected refund replay", err)
|
|
}
|
|
return item, nil
|
|
}
|
|
if item.Status != payments.RefundPending || item.ProviderID != "" {
|
|
return payments.Refund{}, payments.ErrConflict
|
|
}
|
|
item, found, err = queryRefund(ctx, tx, `UPDATE public.payment_refunds
|
|
SET status = 'rejected', failure_reason = $2, updated_at = now()
|
|
WHERE id = $1 AND status = 'pending' AND provider_id IS NULL
|
|
RETURNING `+refundColumnsNoJoin, id, reason)
|
|
if err != nil {
|
|
return payments.Refund{}, paymentSQLError("reject refund", err)
|
|
}
|
|
if !found {
|
|
return payments.Refund{}, payments.ErrConflict
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return payments.Refund{}, paymentSQLError("commit refund rejection", err)
|
|
}
|
|
return item, nil
|
|
}
|
|
|
|
func (db *Database) RecordPaymentRefund(ctx context.Context, remote payments.RefundTransaction) (payments.Refund, error) {
|
|
if _, err := db.paymentQuerier(); err != nil || db.transactions == nil {
|
|
return payments.Refund{}, payments.ErrUnavailable
|
|
}
|
|
if !validPaymentKey(remote.ID, 32) || !validPaymentKey(remote.OrderID, 32) ||
|
|
remote.MchID == "" || remote.TransactionID == "" || remote.ProviderID == "" ||
|
|
remote.Currency != "CNY" || remote.AmountFen <= 0 || remote.TotalFen <= 0 ||
|
|
(remote.Status != payments.RefundPending && remote.Status != payments.RefundProcessing &&
|
|
remote.Status != payments.RefundSuccess && remote.Status != payments.RefundClosed && remote.Status != payments.RefundAbnormal) {
|
|
return payments.Refund{}, payments.ErrVerification
|
|
}
|
|
tx, err := db.transactions.Begin(ctx)
|
|
if err != nil {
|
|
return payments.Refund{}, paymentSQLError("begin refund reconciliation", err)
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
order, found, err := queryPaymentOrder(ctx, tx, lockPaymentOrderSQL, remote.OrderID)
|
|
if err != nil {
|
|
return payments.Refund{}, paymentSQLError("lock original payment", err)
|
|
}
|
|
if !found {
|
|
return payments.Refund{}, payments.ErrNotFound
|
|
}
|
|
item, found, err := queryRefund(ctx, tx, refundSelect+` WHERE r.id = $1 FOR UPDATE OF r`, remote.ID)
|
|
if err != nil {
|
|
return payments.Refund{}, paymentSQLError("lock refund", err)
|
|
}
|
|
if !found {
|
|
return payments.Refund{}, payments.ErrNotFound
|
|
}
|
|
if order.Status != payments.StatusPaid || item.OrderID != order.ID || item.OrganizationID != order.OrganizationID ||
|
|
item.MchID != order.MchID || item.TransactionID != order.TransactionID || item.TotalFen != order.AmountFen ||
|
|
item.ID != remote.ID || item.OrderID != remote.OrderID || item.MchID != remote.MchID ||
|
|
item.TransactionID != remote.TransactionID || item.AmountFen != remote.AmountFen ||
|
|
item.TotalFen != remote.TotalFen || item.Currency != remote.Currency ||
|
|
(item.ProviderID != "" && item.ProviderID != remote.ProviderID) {
|
|
return payments.Refund{}, payments.ErrVerification
|
|
}
|
|
if item.Status == payments.RefundSuccess {
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return payments.Refund{}, paymentSQLError("commit refund replay", err)
|
|
}
|
|
return item, nil
|
|
}
|
|
if remote.Status == payments.RefundSuccess {
|
|
// A provider-confirmed refund can arrive after a former CLOSED
|
|
// reservation was released and another refund was requested. Pending
|
|
// requests are only local reservations; they cannot invalidate money
|
|
// already returned by WeChat. Count completed refunds for this check.
|
|
used, err := queryRefundSucceeded(ctx, tx, order.ID, item.ID)
|
|
if err != nil {
|
|
return payments.Refund{}, paymentSQLError("check total refunded", err)
|
|
}
|
|
if used > order.AmountFen || item.AmountFen > order.AmountFen-used {
|
|
return payments.Refund{}, payments.ErrConflict
|
|
}
|
|
if err := debitRefundWallet(ctx, tx, item, remote.ProviderID); err != nil {
|
|
return payments.Refund{}, err
|
|
}
|
|
}
|
|
// Once closed, a delayed genuine success may still arrive. Other stale states
|
|
// never reopen it; success itself can never be reversed locally.
|
|
status := remote.Status
|
|
if item.Status == payments.RefundClosed && status != payments.RefundSuccess {
|
|
status = payments.RefundClosed
|
|
}
|
|
if item.Status == payments.RefundRejected && status != payments.RefundSuccess &&
|
|
status != payments.RefundProcessing && status != payments.RefundAbnormal {
|
|
status = payments.RefundRejected
|
|
}
|
|
if item.Status == payments.RefundProcessing && status == payments.RefundPending {
|
|
status = payments.RefundProcessing
|
|
}
|
|
if item.Status == payments.RefundAbnormal && status == payments.RefundPending {
|
|
status = payments.RefundAbnormal
|
|
}
|
|
when := remote.SuccessTime
|
|
if status == payments.RefundSuccess && when.IsZero() {
|
|
when = time.Now().UTC()
|
|
}
|
|
item, found, err = queryRefund(ctx, tx, `UPDATE public.payment_refunds SET
|
|
status = $2, provider_id = $3, succeeded_at = CASE WHEN $2 = 'success' THEN $4::timestamptz ELSE NULL END,
|
|
failure_reason = CASE WHEN $2 = 'rejected' THEN failure_reason ELSE '' END,
|
|
updated_at = now() WHERE id = $1 RETURNING `+refundColumnsNoJoin,
|
|
item.ID, status, remote.ProviderID, nullableRefundTime(status, when))
|
|
if err != nil {
|
|
return payments.Refund{}, paymentSQLError("record refund status", err)
|
|
}
|
|
if !found {
|
|
return payments.Refund{}, payments.ErrConflict
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return payments.Refund{}, paymentSQLError("commit refund reconciliation", err)
|
|
}
|
|
return item, nil
|
|
}
|
|
|
|
func queryRefundSucceeded(ctx context.Context, q Querier, orderID, excludeID string) (int64, error) {
|
|
rows, err := q.Query(ctx, `SELECT COALESCE(sum(amount_fen), 0)::bigint FROM public.payment_refunds
|
|
WHERE order_id = $1 AND id <> $2 AND status = 'success'`, orderID, excludeID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer rows.Close()
|
|
if !rows.Next() {
|
|
return 0, rows.Err()
|
|
}
|
|
var amount int64
|
|
if err := rows.Scan(&amount); err != nil {
|
|
return 0, err
|
|
}
|
|
return amount, rows.Err()
|
|
}
|
|
|
|
func nullableRefundTime(status string, when time.Time) any {
|
|
if status == payments.RefundSuccess {
|
|
return when
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func debitRefundWallet(ctx context.Context, tx Transaction, item payments.Refund, providerID string) error {
|
|
metadata, err := json.Marshal(map[string]string{
|
|
"source": "wechat_refund", "paymentOrderId": item.OrderID, "refundId": item.ID,
|
|
"transactionId": item.TransactionID, "providerRefundId": providerID,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rows, err := tx.Query(ctx, `INSERT INTO public.billing_wallets(organization_id) VALUES ($1)
|
|
ON CONFLICT (organization_id) DO NOTHING RETURNING organization_id`, item.OrganizationID)
|
|
if err != nil {
|
|
return paymentSQLError("ensure refund wallet", err)
|
|
}
|
|
rows.Close()
|
|
rows, err = tx.Query(ctx, `UPDATE public.billing_wallets SET balance_fen = balance_fen - $2,
|
|
updated_at = now() WHERE organization_id = $1 RETURNING balance_fen`, item.OrganizationID, item.AmountFen)
|
|
if err != nil {
|
|
return paymentSQLError("debit refund wallet", err)
|
|
}
|
|
if !rows.Next() {
|
|
rows.Close()
|
|
return payments.ErrConflict
|
|
}
|
|
var balance int64
|
|
if err := rows.Scan(&balance); err != nil {
|
|
rows.Close()
|
|
return paymentSQLError("scan refund wallet", err)
|
|
}
|
|
rows.Close()
|
|
key := "wechat-refund:" + item.MchID + ":" + item.ID
|
|
rows, err = tx.Query(ctx, `INSERT INTO public.billing_ledger
|
|
(id, organization_id, kind, delta_fen, balance_after_fen, currency,
|
|
idempotency_key, description, metadata)
|
|
VALUES ($1, $2, 'recharge_refund', $3, $4, 'CNY', $5, '微信充值退款', $6::jsonb)
|
|
RETURNING id`, "wechat-refund-entry:"+item.ID, item.OrganizationID, -item.AmountFen,
|
|
balance, key, json.RawMessage(metadata))
|
|
if err != nil {
|
|
return paymentSQLError("record refund wallet ledger", err)
|
|
}
|
|
if !rows.Next() {
|
|
rows.Close()
|
|
return payments.ErrConflict
|
|
}
|
|
var id string
|
|
if err := rows.Scan(&id); err != nil {
|
|
rows.Close()
|
|
return paymentSQLError("scan refund ledger", err)
|
|
}
|
|
rows.Close()
|
|
if id != "wechat-refund-entry:"+item.ID {
|
|
return payments.ErrConflict
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func queryRefund(ctx context.Context, q Querier, statement string, args ...any) (payments.Refund, bool, error) {
|
|
rows, err := q.Query(ctx, statement, args...)
|
|
if err != nil {
|
|
return payments.Refund{}, false, err
|
|
}
|
|
defer rows.Close()
|
|
if !rows.Next() {
|
|
return payments.Refund{}, false, rows.Err()
|
|
}
|
|
item, err := scanRefund(rows)
|
|
if err != nil {
|
|
return payments.Refund{}, false, err
|
|
}
|
|
return item, true, rows.Err()
|
|
}
|
|
|
|
func queryRefunds(ctx context.Context, q Querier, statement string, args ...any) ([]payments.Refund, error) {
|
|
rows, err := q.Query(ctx, statement, args...)
|
|
if err != nil {
|
|
return nil, paymentSQLError("list refunds", err)
|
|
}
|
|
defer rows.Close()
|
|
out := make([]payments.Refund, 0)
|
|
for rows.Next() {
|
|
item, err := scanRefund(rows)
|
|
if err != nil {
|
|
return nil, paymentSQLError("scan refund", err)
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
return out, paymentSQLError("read refunds", rows.Err())
|
|
}
|
|
|
|
func scanRefund(rows Rows) (payments.Refund, error) {
|
|
var item payments.Refund
|
|
var succeeded sql.NullTime
|
|
if err := rows.Scan(&item.ID, &item.OrderID, &item.OrganizationID, &item.OperatorID,
|
|
&item.OperatorName, &item.RequestKey, &item.MchID, &item.TransactionID,
|
|
&item.ProviderID, &item.AmountFen, &item.TotalFen, &item.Currency, &item.Reason,
|
|
&item.Status, &item.FailureReason, &item.CreatedAt, &item.UpdatedAt, &succeeded); err != nil {
|
|
return payments.Refund{}, err
|
|
}
|
|
if succeeded.Valid {
|
|
item.SucceededAt = &succeeded.Time
|
|
}
|
|
return item, nil
|
|
}
|
|
|
|
var _ payments.RefundRepository = (*Database)(nil)
|