7860efce48
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.
43 lines
1.2 KiB
Go
43 lines
1.2 KiB
Go
package payments
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// gateOnlyService builds a Service with a fixed clock and no store, usable only for the CreateOrder
|
|
// gate rejections that return before any store access.
|
|
func gateOnlyService() *Service {
|
|
return &Service{clock: func() time.Time { return time.Unix(0, 0).UTC() }}
|
|
}
|
|
|
|
// TestCreateOrderGateRejections checks that CreateOrder fails closed — before touching the store —
|
|
// on an untrusted platform, the VK-iOS spend freeze, and a method whose funding segment the account
|
|
// does not hold.
|
|
func TestCreateOrderGateRejections(t *testing.T) {
|
|
ctx := context.Background()
|
|
acc, prod := uuid.New(), uuid.New()
|
|
present := []Source{SourceDirect, SourceVK}
|
|
|
|
cases := []struct {
|
|
name string
|
|
cxt Context
|
|
}{
|
|
{"untrusted context", Context{}},
|
|
{"vk-ios spend freeze", Context{Kind: SourceVK, Subtype: SubtypeIOS}},
|
|
{"method segment not attached", Context{Kind: SourceTelegram}},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := gateOnlyService().CreateOrder(ctx, acc, tc.cxt, present, prod, "robokassa")
|
|
if !errors.Is(err, ErrUntrusted) {
|
|
t.Fatalf("CreateOrder(%s) = %v, want ErrUntrusted", tc.name, err)
|
|
}
|
|
})
|
|
}
|
|
}
|