feat(payments): order-flow and fund credit engine + the Robokassa adapter
Add the payment-intake write path (provider-agnostic) and the Robokassa direct-rail glue, both unit-tested; transport, wire and UI follow. - payments: extend the ledger insert to thread order_id/provider/ provider_payment_id (spend/grant pass nil); add the order store (create/read/expire + a pack-price loader) and the fund credit — a fund ledger row + a guarded balance upsert + mark-paid in one tx, idempotent on the (provider, provider_payment_id) unique index, cache invalidated after commit. A valid callback is honoured even on an expired order. Service CreateOrder/Fund/ExpireOrders; Money.Major for the provider amount field. - robokassa: build the signed hosted-payment URL (SHA-256, order id via Shp_order, InvId unused) and verify the Result callback signature (Password2), extracting the order and amount. Receipt/fiscalisation is configured shop-side, so no Receipt parameter is sent.
This commit is contained in:
@@ -230,8 +230,11 @@ func applyBenefitTx(ctx context.Context, tx *sql.Tx, accountID uuid.UUID, origin
|
||||
return nil
|
||||
}
|
||||
|
||||
// insertLedgerTx appends one append-only ledger row inside tx.
|
||||
func insertLedgerTx(ctx context.Context, tx *sql.Tx, accountID uuid.UUID, kind string, source, origin *Source, chipsDelta int, productID *uuid.UUID, snapshot []byte, now time.Time) error {
|
||||
// insertLedgerTx appends one append-only ledger row inside tx. orderID, provider and
|
||||
// providerPaymentID are set only on an intake credit (fund/refund) and are nil for a
|
||||
// spend/admin_grant; a non-nil (provider, providerPaymentID) pair is guarded by the partial
|
||||
// unique index, so a duplicate provider callback fails here (the idempotency key).
|
||||
func insertLedgerTx(ctx context.Context, tx *sql.Tx, accountID uuid.UUID, kind string, source, origin *Source, chipsDelta int, productID, orderID *uuid.UUID, provider, providerPaymentID *string, snapshot []byte, now time.Time) error {
|
||||
id, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return fmt.Errorf("payments: ledger id: %w", err)
|
||||
@@ -246,11 +249,13 @@ func insertLedgerTx(ctx context.Context, tx *sql.Tx, accountID uuid.UUID, kind s
|
||||
stmt := table.Ledger.INSERT(
|
||||
table.Ledger.LedgerID, table.Ledger.AccountID, table.Ledger.Kind,
|
||||
table.Ledger.Source, table.Ledger.Origin, table.Ledger.ChipsDelta,
|
||||
table.Ledger.ProductID, table.Ledger.Snapshot, table.Ledger.CreatedAt,
|
||||
table.Ledger.ProductID, table.Ledger.OrderID, table.Ledger.Provider,
|
||||
table.Ledger.ProviderPaymentID, table.Ledger.Snapshot, table.Ledger.CreatedAt,
|
||||
).VALUES(
|
||||
id, accountID, kind,
|
||||
sourceOrNull(source), sourceOrNull(origin), int32(chipsDelta),
|
||||
uuidOrNull(productID), snap, now,
|
||||
uuidOrNull(productID), uuidOrNull(orderID), stringOrNull(provider),
|
||||
stringOrNull(providerPaymentID), snap, now,
|
||||
)
|
||||
if _, err := stmt.ExecContext(ctx, tx); err != nil {
|
||||
return fmt.Errorf("payments: insert %s ledger: %w", kind, err)
|
||||
@@ -278,7 +283,7 @@ func (s *Store) spend(ctx context.Context, accountID uuid.UUID, draws []sourceAm
|
||||
return ErrInsufficientChips
|
||||
}
|
||||
src := dr.source
|
||||
if err := insertLedgerTx(ctx, tx, accountID, "spend", &src, &origin, -dr.amount, &productID, snapshot, now); err != nil {
|
||||
if err := insertLedgerTx(ctx, tx, accountID, "spend", &src, &origin, -dr.amount, &productID, nil, nil, nil, snapshot, now); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -295,7 +300,7 @@ func (s *Store) spend(ctx context.Context, accountID uuid.UUID, draws []sourceAm
|
||||
// the chosen origin, in one transaction — a zero-price sale of a value.
|
||||
func (s *Store) grant(ctx context.Context, accountID uuid.UUID, origin Source, d benefitDelta, snapshot []byte, now time.Time) error {
|
||||
err := withTx(ctx, s.db, func(tx *sql.Tx) error {
|
||||
if err := insertLedgerTx(ctx, tx, accountID, "admin_grant", nil, &origin, 0, nil, snapshot, now); err != nil {
|
||||
if err := insertLedgerTx(ctx, tx, accountID, "admin_grant", nil, &origin, 0, nil, nil, nil, nil, snapshot, now); err != nil {
|
||||
return err
|
||||
}
|
||||
return applyBenefitTx(ctx, tx, accountID, origin, d, now)
|
||||
@@ -419,3 +424,11 @@ func uuidOrNull(id *uuid.UUID) postgres.Expression {
|
||||
}
|
||||
return postgres.UUID(*id)
|
||||
}
|
||||
|
||||
// stringOrNull renders an optional string as a SQL string or NULL.
|
||||
func stringOrNull(s *string) postgres.Expression {
|
||||
if s == nil {
|
||||
return postgres.NULL
|
||||
}
|
||||
return postgres.String(*s)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user