mirror of
https://github.com/hanzo-fi/payments.git
synced 2026-10-30 20:31:20 +00:00
331 lines
15 KiB
Go
331 lines
15 KiB
Go
//go:build it
|
|
|
|
package test_suite
|
|
|
|
import (
|
|
"math/big"
|
|
"time"
|
|
|
|
"github.com/hanzo-fi/go-libs/v5/pkg/observe/log"
|
|
"github.com/hanzo-fi/go-libs/v5/pkg/testing/deferred"
|
|
"github.com/hanzo-fi/go-libs/v5/pkg/types/pointer"
|
|
"github.com/hanzo-fi/payments/internal/connectors/engine/workflow"
|
|
"github.com/hanzo-fi/payments/pkg/domain/models"
|
|
"github.com/hanzo-fi/payments/pkg/client/models/components"
|
|
"github.com/hanzo-fi/payments/pkg/events"
|
|
"github.com/google/uuid"
|
|
|
|
. "github.com/hanzo-fi/payments/pkg/testserver"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Context("Payments API Payment Initiation", Serial, func() {
|
|
var (
|
|
db = UseTemplatedDatabase()
|
|
ctx = logging.TestingContext()
|
|
|
|
app *deferred.Deferred[*Server]
|
|
)
|
|
|
|
app = NewTestServer(func() Configuration {
|
|
return Configuration{
|
|
Stack: stack,
|
|
PostgresConfiguration: db.GetValue().ConnectionOptions(),
|
|
NatsURL: natsServer.GetValue().ClientURL(),
|
|
TemporalNamespace: temporalServer.GetValue().DefaultNamespace(),
|
|
TemporalAddress: temporalServer.GetValue().Address(),
|
|
Output: GinkgoWriter,
|
|
SkipOutboxScheduleCreation: true,
|
|
}
|
|
})
|
|
|
|
createdAt, _ := time.Parse("2006-Jan-02", "2024-Nov-29")
|
|
|
|
AfterEach(func() {
|
|
flushRemainingWorkflows(ctx)
|
|
})
|
|
|
|
When("initiating a new transfer with v3", func() {
|
|
var (
|
|
err error
|
|
|
|
debtorID string
|
|
creditorID string
|
|
payReq *components.V3InitiatePaymentRequest
|
|
paymentInitiationID string
|
|
|
|
connectorID string
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
connectorID, err = installConnector(ctx, app.GetValue(), uuid.New(), 3)
|
|
Expect(err).To(BeNil())
|
|
|
|
debtorID, creditorID = setupDebtorAndCreditorV3Accounts(ctx, app.GetValue(), connectorID, createdAt)
|
|
payReq = &components.V3InitiatePaymentRequest{
|
|
Reference: uuid.New().String(),
|
|
ConnectorID: connectorID,
|
|
Description: "some description",
|
|
Type: "TRANSFER",
|
|
Amount: big.NewInt(3200),
|
|
Asset: "EUR/2",
|
|
SourceAccountID: &debtorID,
|
|
DestinationAccountID: &creditorID,
|
|
Metadata: map[string]string{"key": "val"},
|
|
}
|
|
|
|
createResponse, err := app.GetValue().SDK().Payments.V3.InitiatePayment(ctx, pointer.For(false), payReq)
|
|
Expect(err).To(BeNil())
|
|
Expect(createResponse.GetV3InitiatePaymentResponse().Data.TaskID).ToNot(BeNil())
|
|
Expect(*createResponse.GetV3InitiatePaymentResponse().Data.TaskID).To(Equal("")) // task empty when not sending to PSP
|
|
Expect(createResponse.GetV3InitiatePaymentResponse().Data.PaymentInitiationID).ToNot(BeNil()) // task nil when not sending to PSP
|
|
paymentInitiationID = *createResponse.GetV3InitiatePaymentResponse().Data.PaymentInitiationID
|
|
MustOutbox(ctx, app.GetValue(), events.EventTypeSavedPaymentInitiation)
|
|
var msg = struct {
|
|
PaymentInitiationID string `json:"paymentInitiationID"`
|
|
Status string `json:"status"`
|
|
}{
|
|
PaymentInitiationID: paymentInitiationID,
|
|
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_WAITING_FOR_VALIDATION.String(),
|
|
}
|
|
MustOutbox(ctx, app.GetValue(), events.EventTypeSavedPaymentInitiationAdjustment, WithPayloadSubset(msg))
|
|
})
|
|
|
|
AfterEach(func() {
|
|
uninstallConnector(ctx, app.GetValue(), connectorID)
|
|
})
|
|
|
|
It("can be processed", func() {
|
|
approveResponse, err := app.GetValue().SDK().Payments.V3.ApprovePaymentInitiation(ctx, paymentInitiationID)
|
|
Expect(err).To(BeNil())
|
|
Expect(approveResponse.GetV3ApprovePaymentInitiationResponse().Data).NotTo(BeNil())
|
|
taskID, err := models.TaskIDFromString(approveResponse.GetV3ApprovePaymentInitiationResponse().Data.TaskID)
|
|
Expect(err).To(BeNil())
|
|
Expect(taskID.Reference).To(ContainSubstring("create-transfer"))
|
|
|
|
var paymentMsg = struct {
|
|
ConnectorID string `json:"connectorID"`
|
|
SourceAccountID string `json:"sourceAccountID,omitempty"`
|
|
DestinationAccountID string `json:"destinationAccountID,omitempty"`
|
|
}{
|
|
ConnectorID: connectorID,
|
|
SourceAccountID: debtorID,
|
|
DestinationAccountID: creditorID,
|
|
}
|
|
|
|
type PIAdjMsg struct {
|
|
PaymentInitiationID string `json:"paymentInitiationID"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
processingPI := PIAdjMsg{
|
|
PaymentInitiationID: paymentInitiationID,
|
|
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING.String(),
|
|
}
|
|
MustEventuallyOutbox(ctx, app.GetValue(), events.EventTypeSavedPaymentInitiationAdjustment, WithPayloadSubset(processingPI))
|
|
MustEventuallyOutbox(ctx, app.GetValue(), events.EventTypeSavedPayments, WithPayloadSubset(paymentMsg))
|
|
MustEventuallyOutbox(ctx, app.GetValue(), events.EventTypeSavedPaymentInitiationRelatedPayment)
|
|
processedPI := PIAdjMsg{
|
|
PaymentInitiationID: paymentInitiationID,
|
|
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSED.String(),
|
|
}
|
|
MustEventuallyOutbox(ctx, app.GetValue(), events.EventTypeSavedPaymentInitiationAdjustment, WithPayloadSubset(processedPI))
|
|
taskPoller := TaskPoller(ctx, GinkgoT(), app.GetValue())
|
|
Eventually(taskPoller(approveResponse.GetV3ApprovePaymentInitiationResponse().Data.TaskID)).WithTimeout(2 * time.Second).Should(HaveTaskStatus(models.TASK_STATUS_SUCCEEDED))
|
|
|
|
getResponse, err := app.GetValue().SDK().Payments.V3.GetPaymentInitiation(ctx, paymentInitiationID)
|
|
Expect(err).To(BeNil())
|
|
Expect(string(getResponse.GetV3GetPaymentInitiationResponse().Data.Status)).To(Equal(models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSED.String()))
|
|
})
|
|
|
|
It("can be rejected", func() {
|
|
_, err := app.GetValue().SDK().Payments.V3.RejectPaymentInitiation(ctx, paymentInitiationID)
|
|
Expect(err).To(BeNil())
|
|
|
|
getResponse, err := app.GetValue().SDK().Payments.V3.GetPaymentInitiation(ctx, paymentInitiationID)
|
|
Expect(err).To(BeNil())
|
|
Expect(err).To(BeNil())
|
|
Expect(string(getResponse.GetV3GetPaymentInitiationResponse().Data.Status)).To(Equal(models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_REJECTED.String()))
|
|
})
|
|
|
|
It("cannot be reversed if the payment is unprocessed", func() {
|
|
reverseResponse, err := app.GetValue().SDK().Payments.V3.ReversePaymentInitiation(ctx, paymentInitiationID, &components.V3ReversePaymentInitiationRequest{
|
|
Reference: uuid.New().String(),
|
|
Description: payReq.Description,
|
|
Amount: payReq.Amount,
|
|
Asset: payReq.Asset,
|
|
Metadata: map[string]string{"reversal": "data"},
|
|
})
|
|
Expect(err).To(BeNil())
|
|
Expect(reverseResponse.GetV3ReversePaymentInitiationResponse().Data.TaskID).NotTo(BeNil())
|
|
taskPoller := TaskPoller(ctx, GinkgoT(), app.GetValue())
|
|
Eventually(taskPoller(*reverseResponse.GetV3ReversePaymentInitiationResponse().Data.TaskID)).WithTimeout(2 * time.Second).Should(HaveTaskStatus(models.TASK_STATUS_FAILED, WithError(workflow.ErrPaymentInitiationNotProcessed)))
|
|
})
|
|
|
|
It("can be reversed", func() {
|
|
approveResponse, err := app.GetValue().SDK().Payments.V3.ApprovePaymentInitiation(ctx, paymentInitiationID)
|
|
Expect(err).To(BeNil())
|
|
|
|
var msg = struct {
|
|
ConnectorID string `json:"connectorID"`
|
|
SourceAccountID string `json:"sourceAccountID,omitempty"`
|
|
DestinationAccountID string `json:"destinationAccountID,omitempty"`
|
|
}{
|
|
ConnectorID: connectorID,
|
|
SourceAccountID: debtorID,
|
|
DestinationAccountID: creditorID,
|
|
}
|
|
MustEventuallyOutbox(ctx, app.GetValue(), events.EventTypeSavedPayments, WithPayloadSubset(msg))
|
|
taskPoller := TaskPoller(ctx, GinkgoT(), app.GetValue())
|
|
Eventually(taskPoller(approveResponse.GetV3ApprovePaymentInitiationResponse().Data.TaskID)).
|
|
WithTimeout(2 * time.Second).
|
|
Should(HaveTaskStatus(models.TASK_STATUS_SUCCEEDED))
|
|
|
|
reverseResponse, err := app.GetValue().SDK().Payments.V3.ReversePaymentInitiation(ctx, paymentInitiationID, &components.V3ReversePaymentInitiationRequest{
|
|
Reference: uuid.New().String(),
|
|
Description: payReq.Description,
|
|
Amount: payReq.Amount,
|
|
Asset: payReq.Asset,
|
|
Metadata: map[string]string{"reversal": "data"},
|
|
})
|
|
Expect(err).To(BeNil())
|
|
Expect(reverseResponse.GetV3ReversePaymentInitiationResponse().Data.TaskID).NotTo(BeNil())
|
|
blockTillWorkflowComplete(ctx, connectorID, "reverse-transfer")
|
|
Eventually(taskPoller(*reverseResponse.GetV3ReversePaymentInitiationResponse().Data.TaskID)).
|
|
WithTimeout(2 * time.Second).
|
|
Should(HaveTaskStatus(models.TASK_STATUS_SUCCEEDED))
|
|
|
|
getResponse, err := app.GetValue().SDK().Payments.V3.GetPaymentInitiation(ctx, paymentInitiationID)
|
|
Expect(err).To(BeNil())
|
|
Expect(string(getResponse.GetV3GetPaymentInitiationResponse().Data.Status)).To(Equal(models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_REVERSED.String()))
|
|
})
|
|
})
|
|
|
|
When("initiating a new payout with v3", func() {
|
|
var (
|
|
err error
|
|
|
|
debtorID string
|
|
creditorID string
|
|
payReq *components.V3InitiatePaymentRequest
|
|
paymentInitiationID string
|
|
|
|
connectorID string
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
connectorID, err = installConnector(ctx, app.GetValue(), uuid.New(), 3)
|
|
Expect(err).To(BeNil())
|
|
|
|
debtorID, creditorID = setupDebtorAndCreditorV3Accounts(ctx, app.GetValue(), connectorID, createdAt)
|
|
payReq = &components.V3InitiatePaymentRequest{
|
|
Reference: uuid.New().String(),
|
|
ConnectorID: connectorID,
|
|
Description: "payout description",
|
|
Type: "PAYOUT",
|
|
Amount: big.NewInt(2233),
|
|
Asset: "EUR/2",
|
|
SourceAccountID: &debtorID,
|
|
DestinationAccountID: &creditorID,
|
|
Metadata: map[string]string{"pay": "out"},
|
|
}
|
|
|
|
createResponse, err := app.GetValue().SDK().Payments.V3.InitiatePayment(ctx, pointer.For(false), payReq)
|
|
Expect(err).To(BeNil())
|
|
Expect(createResponse.GetV3InitiatePaymentResponse().Data.TaskID).ToNot(BeNil()) // task nil when not sending to PSP
|
|
Expect(*createResponse.GetV3InitiatePaymentResponse().Data.TaskID).To(Equal("")) // task nil when not sending to PSP
|
|
Expect(createResponse.GetV3InitiatePaymentResponse().Data.PaymentInitiationID).ToNot(BeNil()) // task nil when not sending to PSP
|
|
paymentInitiationID = *createResponse.GetV3InitiatePaymentResponse().Data.PaymentInitiationID
|
|
})
|
|
|
|
AfterEach(func() {
|
|
uninstallConnector(ctx, app.GetValue(), connectorID)
|
|
})
|
|
|
|
It("can be processed", func() {
|
|
approveResponse, err := app.GetValue().SDK().Payments.V3.ApprovePaymentInitiation(ctx, paymentInitiationID)
|
|
Expect(err).To(BeNil())
|
|
Expect(approveResponse.GetV3ApprovePaymentInitiationResponse().Data).NotTo(BeNil())
|
|
taskID, err := models.TaskIDFromString(approveResponse.GetV3ApprovePaymentInitiationResponse().Data.TaskID)
|
|
Expect(err).To(BeNil())
|
|
Expect(taskID.Reference).To(ContainSubstring("create-payout"))
|
|
|
|
var msg = struct {
|
|
ConnectorID string `json:"connectorID"`
|
|
SourceAccountID string `json:"sourceAccountID,omitempty"`
|
|
DestinationAccountID string `json:"destinationAccountID,omitempty"`
|
|
}{
|
|
ConnectorID: connectorID,
|
|
SourceAccountID: debtorID,
|
|
DestinationAccountID: creditorID,
|
|
}
|
|
MustEventuallyOutbox(ctx, app.GetValue(), events.EventTypeSavedPayments, WithPayloadSubset(msg))
|
|
taskPoller := TaskPoller(ctx, GinkgoT(), app.GetValue())
|
|
Eventually(taskPoller(approveResponse.GetV3ApprovePaymentInitiationResponse().Data.TaskID)).
|
|
WithTimeout(2 * time.Second).Should(HaveTaskStatus(models.TASK_STATUS_SUCCEEDED))
|
|
|
|
getResponse, err := app.GetValue().SDK().Payments.V3.GetPaymentInitiation(ctx, paymentInitiationID)
|
|
Expect(err).To(BeNil())
|
|
Expect(string(getResponse.GetV3GetPaymentInitiationResponse().Data.Status)).To(Equal(models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSED.String()))
|
|
})
|
|
|
|
It("can be rejected", func() {
|
|
_, err := app.GetValue().SDK().Payments.V3.RejectPaymentInitiation(ctx, paymentInitiationID)
|
|
Expect(err).To(BeNil())
|
|
|
|
getResponse, err := app.GetValue().SDK().Payments.V3.GetPaymentInitiation(ctx, paymentInitiationID)
|
|
Expect(err).To(BeNil())
|
|
Expect(string(getResponse.GetV3GetPaymentInitiationResponse().Data.Status)).To(Equal(models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_REJECTED.String()))
|
|
})
|
|
|
|
It("cannot be reversed if the payment is unprocessed", func() {
|
|
reverseResponse, err := app.GetValue().SDK().Payments.V3.ReversePaymentInitiation(ctx, paymentInitiationID, &components.V3ReversePaymentInitiationRequest{
|
|
Reference: uuid.New().String(),
|
|
Description: payReq.Description,
|
|
Amount: payReq.Amount,
|
|
Asset: payReq.Asset,
|
|
Metadata: map[string]string{"reversal": "data"},
|
|
})
|
|
Expect(err).To(BeNil())
|
|
Expect(reverseResponse.GetV3ReversePaymentInitiationResponse().Data.TaskID).NotTo(BeNil())
|
|
taskPoller := TaskPoller(ctx, GinkgoT(), app.GetValue())
|
|
Eventually(taskPoller(*reverseResponse.GetV3ReversePaymentInitiationResponse().Data.TaskID)).WithTimeout(2 * time.Second).Should(HaveTaskStatus(models.TASK_STATUS_FAILED, WithError(workflow.ErrPaymentInitiationNotProcessed)))
|
|
})
|
|
|
|
It("can be reversed", func() {
|
|
approveResponse, err := app.GetValue().SDK().Payments.V3.ApprovePaymentInitiation(ctx, paymentInitiationID)
|
|
Expect(err).To(BeNil())
|
|
|
|
var msg = struct {
|
|
ConnectorID string `json:"connectorID"`
|
|
SourceAccountID string `json:"sourceAccountID,omitempty"`
|
|
DestinationAccountID string `json:"destinationAccountID,omitempty"`
|
|
}{
|
|
ConnectorID: connectorID,
|
|
SourceAccountID: debtorID,
|
|
DestinationAccountID: creditorID,
|
|
}
|
|
MustEventuallyOutbox(ctx, app.GetValue(), events.EventTypeSavedPayments, WithPayloadSubset(msg))
|
|
taskPoller := TaskPoller(ctx, GinkgoT(), app.GetValue())
|
|
Eventually(taskPoller(approveResponse.GetV3ApprovePaymentInitiationResponse().Data.TaskID)).WithTimeout(2 * time.Second).Should(HaveTaskStatus(models.TASK_STATUS_SUCCEEDED))
|
|
|
|
reverseResponse, err := app.GetValue().SDK().Payments.V3.ReversePaymentInitiation(ctx, paymentInitiationID, &components.V3ReversePaymentInitiationRequest{
|
|
Reference: uuid.New().String(),
|
|
Description: payReq.Description,
|
|
Amount: payReq.Amount,
|
|
Asset: payReq.Asset,
|
|
Metadata: map[string]string{"reversal": "data"},
|
|
})
|
|
Expect(err).To(BeNil())
|
|
Expect(reverseResponse.GetV3ReversePaymentInitiationResponse().Data.TaskID).NotTo(BeNil())
|
|
blockTillWorkflowComplete(ctx, connectorID, "reverse-payout")
|
|
Eventually(taskPoller(*reverseResponse.GetV3ReversePaymentInitiationResponse().Data.TaskID)).WithTimeout(2 * time.Second).Should(HaveTaskStatus(models.TASK_STATUS_SUCCEEDED))
|
|
|
|
getResponse, err := app.GetValue().SDK().Payments.V3.GetPaymentInitiation(ctx, paymentInitiationID)
|
|
Expect(err).To(BeNil())
|
|
Expect(string(getResponse.GetV3GetPaymentInitiationResponse().Data.Status)).To(Equal(models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_REVERSED.String()))
|
|
})
|
|
})
|
|
})
|