Skip to content

Commit

Permalink
Merge pull request #120 from formancehq/feat/payments-v3-add-payments…
Browse files Browse the repository at this point in the history
…-creation

 feat(payments): add payments creation from API
  • Loading branch information
paul-nicolas authored Oct 11, 2024
2 parents 7631203 + 5c811f2 commit 7a07fdb
Show file tree
Hide file tree
Showing 13 changed files with 606 additions and 2 deletions.
1 change: 1 addition & 0 deletions internal/api/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type Backend interface {
ConnectorsReset(ctx context.Context, connectorID models.ConnectorID) error

// Payments
PaymentsCreate(ctx context.Context, payment models.Payment) error
PaymentsUpdateMetadata(ctx context.Context, id models.PaymentID, metadata map[string]string) error
PaymentsList(ctx context.Context, query storage.ListPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error)
PaymentsGet(ctx context.Context, id models.PaymentID) (*models.Payment, error)
Expand Down
28 changes: 28 additions & 0 deletions internal/api/backend/backend_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions internal/api/services/payments_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package services

import (
"context"

"github.com/formancehq/payments/internal/models"
)

func (s *Service) PaymentsCreate(ctx context.Context, payment models.Payment) error {
return handleEngineErrors(s.engine.CreateFormancePayment(ctx, payment))
}
226 changes: 226 additions & 0 deletions internal/api/v2/handler_payments_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
package v2

import (
"encoding/json"
"errors"
"fmt"
"math/big"
"net/http"
"time"

"github.com/formancehq/go-libs/api"
"github.com/formancehq/go-libs/pointer"
"github.com/formancehq/payments/internal/api/backend"
"github.com/formancehq/payments/internal/models"
"github.com/formancehq/payments/internal/otel"
)

type createPaymentRequest struct {
Reference string `json:"reference"`
ConnectorID string `json:"connectorID"`
CreatedAt time.Time `json:"createdAt"`
Type string `json:"type"`
Amount *big.Int `json:"amount"`
Asset string `json:"asset"`
Scheme string `json:"scheme"`
Status string `json:"status"`
SourceAccountID *string `json:"sourceAccountID"`
DestinationAccountID *string `json:"destinationAccountID"`
Metadata map[string]string `json:"metadata"`
}

func (r *createPaymentRequest) validate() error {
if r.Reference == "" {
return errors.New("reference is required")
}

if r.ConnectorID == "" {
return errors.New("connectorID is required")
}

if r.CreatedAt.IsZero() || r.CreatedAt.After(time.Now()) {
return errors.New("createdAt is empty or in the future")
}

if r.Amount == nil {
return errors.New("amount is required")
}

if r.Type == "" {
return errors.New("type is required")
}

if _, err := models.PaymentTypeFromString(r.Type); err != nil {
return fmt.Errorf("invalid type: %w", err)
}

if r.Scheme == "" {
return errors.New("scheme is required")
}

if _, err := models.PaymentSchemeFromString(r.Scheme); err != nil {
return fmt.Errorf("invalid scheme: %w", err)
}

if r.Asset == "" {
return errors.New("asset is required")
}

if r.Status == "" {
return errors.New("status is required")
}

if _, err := models.PaymentStatusFromString(r.Status); err != nil {
return fmt.Errorf("invalid status: %w", err)
}

if r.SourceAccountID != nil {
_, err := models.AccountIDFromString(*r.SourceAccountID)
if err != nil {
return fmt.Errorf("invalid sourceAccountID: %w", err)
}
}

if r.DestinationAccountID != nil {
_, err := models.AccountIDFromString(*r.DestinationAccountID)
if err != nil {
return fmt.Errorf("invalid destinationAccountID: %w", err)
}
}

return nil
}

func paymentsCreate(backend backend.Backend) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, span := otel.Tracer().Start(r.Context(), "v2_paymentsCreate")
defer span.End()

var req createPaymentRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
otel.RecordError(span, err)
api.BadRequest(w, ErrMissingOrInvalidBody, err)
return
}

if err := req.validate(); err != nil {
otel.RecordError(span, err)
api.BadRequest(w, ErrValidation, err)
return
}

connectorID := models.MustConnectorIDFromString(req.ConnectorID)
paymentType := models.MustPaymentTypeFromString(req.Type)
status := models.MustPaymentStatusFromString(req.Status)
raw, err := json.Marshal(req)
if err != nil {
otel.RecordError(span, err)
api.InternalServerError(w, r, err)
return
}
pid := models.PaymentID{
PaymentReference: models.PaymentReference{
Reference: req.Reference,
Type: paymentType,
},
ConnectorID: connectorID,
}

payment := models.Payment{
ID: pid,
ConnectorID: connectorID,
Reference: req.Reference,
CreatedAt: req.CreatedAt.UTC(),
Type: paymentType,
InitialAmount: req.Amount,
Amount: req.Amount,
Asset: req.Asset,
Scheme: models.MustPaymentSchemeFromString(req.Scheme),
SourceAccountID: func() *models.AccountID {
if req.SourceAccountID == nil {
return nil
}
return pointer.For(models.MustAccountIDFromString(*req.SourceAccountID))
}(),
DestinationAccountID: func() *models.AccountID {
if req.DestinationAccountID == nil {
return nil
}
return pointer.For(models.MustAccountIDFromString(*req.DestinationAccountID))
}(),
Metadata: req.Metadata,
}

// Create adjustments from main payments to keep the compatibility with the old API
payment.Adjustments = []models.PaymentAdjustment{
{
ID: models.PaymentAdjustmentID{
PaymentID: pid,
Reference: req.Reference,
CreatedAt: req.CreatedAt,
Status: status,
},
PaymentID: pid,
Reference: req.Reference,
CreatedAt: req.CreatedAt,
Status: status,
Amount: req.Amount,
Asset: &req.Asset,
Metadata: req.Metadata,
Raw: raw,
},
}

err = backend.PaymentsCreate(ctx, payment)
if err != nil {
otel.RecordError(span, err)
handleServiceErrors(w, r, err)
return
}

// Compatibility with old API
data := paymentResponse{
ID: payment.ID.String(),
Reference: payment.Reference,
Type: payment.Type.String(),
Provider: payment.ConnectorID.Provider,
ConnectorID: payment.ConnectorID.String(),
Status: payment.Status.String(),
Amount: payment.Amount,
InitialAmount: payment.InitialAmount,
Scheme: payment.Scheme.String(),
Asset: payment.Asset,
CreatedAt: payment.CreatedAt,
Metadata: payment.Metadata,
}

if payment.SourceAccountID != nil {
data.SourceAccountID = payment.SourceAccountID.String()
}

if payment.DestinationAccountID != nil {
data.DestinationAccountID = payment.DestinationAccountID.String()
}

data.Adjustments = make([]paymentAdjustment, len(payment.Adjustments))
for i := range payment.Adjustments {
data.Adjustments[i] = paymentAdjustment{
Reference: payment.Adjustments[i].ID.Reference,
CreatedAt: payment.Adjustments[i].CreatedAt,
Status: payment.Adjustments[i].Status.String(),
Amount: payment.Adjustments[i].Amount,
Raw: payment.Adjustments[i].Raw,
}
}

err = json.NewEncoder(w).Encode(api.BaseResponse[paymentResponse]{
Data: &data,
})
if err != nil {
otel.RecordError(span, err)
api.InternalServerError(w, r, err)
return
}
}
}
2 changes: 1 addition & 1 deletion internal/api/v2/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ func newRouter(backend backend.Backend, a auth.Authenticator, debug bool) *chi.M
// Payments
r.Route("/payments", func(r chi.Router) {
r.Get("/", paymentsList(backend))
r.Post("/", paymentsCreate(backend))

r.Route("/{paymentID}", func(r chi.Router) {
r.Get("/", paymentsGet(backend))
r.Patch("/metadata", paymentsUpdateMetadata(backend))
// TODO(polo): add create payment handler
})
})

Expand Down
Loading

0 comments on commit 7a07fdb

Please sign in to comment.