mirror of
https://github.com/hanzo-fi/payments.git
synced 2026-10-30 20:31:20 +00:00
499 lines
16 KiB
Go
499 lines
16 KiB
Go
package adyen
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/big"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/adyen/adyen-go-api-library/v7/src/webhook"
|
|
"github.com/hanzo-fi/go-libs/v5/pkg/types/currency"
|
|
"github.com/hanzo-fi/go-libs/v5/pkg/types/pointer"
|
|
"github.com/hanzo-fi/payments/pkg/domain/models"
|
|
)
|
|
|
|
const (
|
|
webhookHMACMetadataKey = "hmac_key"
|
|
)
|
|
|
|
type supportedWebhook struct {
|
|
urlPath string
|
|
fn func(context.Context, models.TranslateWebhookRequest) (models.TranslateWebhookResponse, error)
|
|
}
|
|
|
|
func (p *Plugin) initWebhookConfig() {
|
|
p.supportedWebhooks = map[string]supportedWebhook{
|
|
"standard": {
|
|
urlPath: "/standard",
|
|
fn: p.translateStandardWebhook,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (p *Plugin) createWebhooks(ctx context.Context, req models.CreateWebhooksRequest) ([]models.PSPWebhookConfig, error) {
|
|
configs := make([]models.PSPWebhookConfig, 0, 1)
|
|
if req.WebhookBaseUrl == "" {
|
|
return configs, fmt.Errorf("STACK_PUBLIC_URL is not set")
|
|
}
|
|
|
|
name := "standard"
|
|
standardConfig := p.supportedWebhooks[name]
|
|
|
|
url, err := url.JoinPath(req.WebhookBaseUrl, standardConfig.urlPath)
|
|
if err != nil {
|
|
return configs, err
|
|
}
|
|
|
|
resp, err := p.client.CreateWebhook(ctx, url, req.ConnectorID)
|
|
if err != nil {
|
|
return configs, err
|
|
}
|
|
|
|
configs = append(configs, models.PSPWebhookConfig{
|
|
Name: name,
|
|
URLPath: standardConfig.urlPath,
|
|
Metadata: map[string]string{
|
|
webhookHMACMetadataKey: resp.HMACKey,
|
|
},
|
|
})
|
|
|
|
return configs, err
|
|
}
|
|
|
|
func (p *Plugin) verifyWebhook(_ context.Context, req models.VerifyWebhookRequest) (models.VerifyWebhookResponse, error) {
|
|
if !p.client.VerifyWebhookBasicAuth(req.Webhook.BasicAuth) {
|
|
return models.VerifyWebhookResponse{}, fmt.Errorf("invalid basic auth: %w", models.ErrWebhookVerification)
|
|
}
|
|
|
|
webhooks, err := p.client.TranslateWebhook(string(req.Webhook.Body))
|
|
if err != nil {
|
|
return models.VerifyWebhookResponse{}, err
|
|
}
|
|
|
|
for _, item := range *webhooks.NotificationItems {
|
|
if !p.client.VerifyWebhookHMAC(item, req.Config.Metadata[webhookHMACMetadataKey]) {
|
|
return models.VerifyWebhookResponse{}, fmt.Errorf("invalid HMAC: %w", models.ErrWebhookVerification)
|
|
}
|
|
}
|
|
|
|
sha := sha256.Sum256(req.Webhook.Body)
|
|
ik := base64.StdEncoding.EncodeToString(sha[:])
|
|
return models.VerifyWebhookResponse{
|
|
WebhookIdempotencyKey: pointer.For(ik),
|
|
}, nil
|
|
}
|
|
|
|
func (p *Plugin) translateStandardWebhook(ctx context.Context, req models.TranslateWebhookRequest) (models.TranslateWebhookResponse, error) {
|
|
webhooks, err := p.client.TranslateWebhook(string(req.Webhook.Body))
|
|
if err != nil {
|
|
return models.TranslateWebhookResponse{}, err
|
|
}
|
|
|
|
responses := make([]models.WebhookResponse, 0, len(*webhooks.NotificationItems))
|
|
for _, item := range *webhooks.NotificationItems {
|
|
var payment *models.PSPPayment
|
|
var err error
|
|
switch item.NotificationRequestItem.EventCode {
|
|
case webhook.EventCodeAuthorisation:
|
|
payment, err = p.handleAuthorisation(item.NotificationRequestItem)
|
|
case webhook.EventCodeAuthorisationAdjustment:
|
|
payment, err = p.handleAuthorisationAdjustment(item.NotificationRequestItem)
|
|
case webhook.EventCodeCancellation:
|
|
payment, err = p.handleCancellation(item.NotificationRequestItem)
|
|
case webhook.EventCodeCapture:
|
|
payment, err = p.handleCapture(item.NotificationRequestItem)
|
|
case webhook.EventCodeCaptureFailed:
|
|
payment, err = p.handleCaptureFailed(item.NotificationRequestItem)
|
|
case webhook.EventCodeRefund:
|
|
payment, err = p.handleRefund(item.NotificationRequestItem)
|
|
case webhook.EventCodeRefundFailed:
|
|
payment, err = p.handleRefundFailed(item.NotificationRequestItem)
|
|
case webhook.EventCodeRefundedReversed:
|
|
payment, err = p.handleRefundedReversed(item.NotificationRequestItem)
|
|
case webhook.EventCodeRefundWithData:
|
|
payment, err = p.handleRefundWithData(item.NotificationRequestItem)
|
|
case webhook.EventCodePayoutThirdparty:
|
|
payment, err = p.handlePayoutThirdparty(item.NotificationRequestItem)
|
|
case webhook.EventCodePayoutDecline:
|
|
payment, err = p.handlePayoutDecline(item.NotificationRequestItem)
|
|
case webhook.EventCodePayoutExpire:
|
|
payment, err = p.handlePayoutExpire(item.NotificationRequestItem)
|
|
}
|
|
if err != nil {
|
|
return models.TranslateWebhookResponse{}, err
|
|
}
|
|
|
|
if payment != nil {
|
|
responses = append(responses, models.WebhookResponse{
|
|
Payment: payment,
|
|
})
|
|
}
|
|
}
|
|
|
|
return models.TranslateWebhookResponse{
|
|
Responses: responses,
|
|
}, nil
|
|
}
|
|
|
|
func (p *Plugin) handleAuthorisation(
|
|
item webhook.NotificationRequestItem,
|
|
) (*models.PSPPayment, error) {
|
|
raw, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal item: %w", err)
|
|
}
|
|
|
|
status := models.PAYMENT_STATUS_AUTHORISATION
|
|
if item.Success == "false" {
|
|
status = models.PAYMENT_STATUS_FAILED
|
|
}
|
|
|
|
payment := models.PSPPayment{
|
|
Reference: item.PspReference,
|
|
CreatedAt: *item.EventDate,
|
|
Type: models.PAYMENT_TYPE_PAYIN,
|
|
Amount: big.NewInt(item.Amount.Value),
|
|
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, item.Amount.Currency),
|
|
Scheme: parseScheme(item.PaymentMethod),
|
|
Status: status,
|
|
DestinationAccountReference: pointer.For(item.MerchantAccountCode),
|
|
Raw: raw,
|
|
}
|
|
|
|
return &payment, nil
|
|
}
|
|
|
|
func (p *Plugin) handleAuthorisationAdjustment(
|
|
item webhook.NotificationRequestItem,
|
|
) (*models.PSPPayment, error) {
|
|
raw, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal item: %w", err)
|
|
}
|
|
|
|
payment := models.PSPPayment{
|
|
ParentReference: item.OriginalReference,
|
|
Reference: item.PspReference,
|
|
CreatedAt: *item.EventDate,
|
|
Type: models.PAYMENT_TYPE_PAYIN,
|
|
Amount: big.NewInt(item.Amount.Value),
|
|
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, item.Amount.Currency),
|
|
Scheme: models.PAYMENT_SCHEME_OTHER,
|
|
Status: models.PAYMENT_STATUS_AMOUNT_ADJUSTMENT,
|
|
DestinationAccountReference: pointer.For(item.MerchantAccountCode),
|
|
Raw: raw,
|
|
}
|
|
return &payment, nil
|
|
}
|
|
|
|
func (p *Plugin) handleCancellation(
|
|
item webhook.NotificationRequestItem,
|
|
) (*models.PSPPayment, error) {
|
|
raw, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal item: %w", err)
|
|
}
|
|
|
|
payment := models.PSPPayment{
|
|
ParentReference: item.OriginalReference,
|
|
Reference: item.PspReference,
|
|
CreatedAt: *item.EventDate,
|
|
Type: models.PAYMENT_TYPE_PAYIN,
|
|
Amount: big.NewInt(item.Amount.Value),
|
|
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, item.Amount.Currency),
|
|
Scheme: parseScheme(item.PaymentMethod),
|
|
Status: models.PAYMENT_STATUS_CANCELLED,
|
|
DestinationAccountReference: pointer.For(item.MerchantAccountCode),
|
|
Raw: raw,
|
|
}
|
|
|
|
return &payment, nil
|
|
}
|
|
|
|
func (p *Plugin) handleCapture(
|
|
item webhook.NotificationRequestItem,
|
|
) (*models.PSPPayment, error) {
|
|
if item.Success == "false" {
|
|
return nil, nil
|
|
}
|
|
|
|
raw, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal item: %w", err)
|
|
}
|
|
|
|
payment := models.PSPPayment{
|
|
ParentReference: item.OriginalReference,
|
|
Reference: item.PspReference,
|
|
CreatedAt: *item.EventDate,
|
|
Type: models.PAYMENT_TYPE_PAYIN,
|
|
Amount: big.NewInt(item.Amount.Value),
|
|
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, item.Amount.Currency),
|
|
Scheme: models.PAYMENT_SCHEME_OTHER,
|
|
Status: models.PAYMENT_STATUS_CAPTURE,
|
|
DestinationAccountReference: pointer.For(item.MerchantAccountCode),
|
|
Raw: raw,
|
|
}
|
|
return &payment, nil
|
|
}
|
|
|
|
func (p *Plugin) handleCaptureFailed(
|
|
item webhook.NotificationRequestItem,
|
|
) (*models.PSPPayment, error) {
|
|
if item.Success == "false" {
|
|
return nil, nil
|
|
}
|
|
|
|
raw, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal item: %w", err)
|
|
}
|
|
|
|
payment := models.PSPPayment{
|
|
ParentReference: item.OriginalReference,
|
|
Reference: item.PspReference,
|
|
CreatedAt: *item.EventDate,
|
|
Type: models.PAYMENT_TYPE_PAYIN,
|
|
Amount: big.NewInt(item.Amount.Value),
|
|
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, item.Amount.Currency),
|
|
Scheme: models.PAYMENT_SCHEME_OTHER,
|
|
Status: models.PAYMENT_STATUS_CAPTURE_FAILED,
|
|
DestinationAccountReference: pointer.For(item.MerchantAccountCode),
|
|
Raw: raw,
|
|
}
|
|
|
|
return &payment, nil
|
|
}
|
|
|
|
func (p *Plugin) handleRefund(
|
|
item webhook.NotificationRequestItem,
|
|
) (*models.PSPPayment, error) {
|
|
if item.Success == "false" {
|
|
return nil, nil
|
|
}
|
|
|
|
raw, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal item: %w", err)
|
|
}
|
|
|
|
payment := models.PSPPayment{
|
|
ParentReference: item.OriginalReference,
|
|
Reference: item.PspReference,
|
|
CreatedAt: *item.EventDate,
|
|
Type: models.PAYMENT_TYPE_PAYIN,
|
|
Amount: big.NewInt(item.Amount.Value),
|
|
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, item.Amount.Currency),
|
|
Scheme: models.PAYMENT_SCHEME_OTHER,
|
|
Status: models.PAYMENT_STATUS_REFUNDED,
|
|
DestinationAccountReference: pointer.For(item.MerchantAccountCode),
|
|
Raw: raw,
|
|
}
|
|
|
|
return &payment, nil
|
|
}
|
|
|
|
func (p *Plugin) handleRefundFailed(
|
|
item webhook.NotificationRequestItem,
|
|
) (*models.PSPPayment, error) {
|
|
if item.Success == "false" {
|
|
return nil, nil
|
|
}
|
|
|
|
raw, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal item: %w", err)
|
|
}
|
|
|
|
payment := models.PSPPayment{
|
|
ParentReference: item.OriginalReference,
|
|
Reference: item.PspReference,
|
|
CreatedAt: *item.EventDate,
|
|
Type: models.PAYMENT_TYPE_PAYIN,
|
|
Amount: big.NewInt(item.Amount.Value),
|
|
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, item.Amount.Currency),
|
|
Scheme: models.PAYMENT_SCHEME_OTHER,
|
|
Status: models.PAYMENT_STATUS_REFUNDED_FAILURE,
|
|
DestinationAccountReference: pointer.For(item.MerchantAccountCode),
|
|
Raw: raw,
|
|
}
|
|
return &payment, nil
|
|
}
|
|
|
|
func (p *Plugin) handleRefundedReversed(
|
|
item webhook.NotificationRequestItem,
|
|
) (*models.PSPPayment, error) {
|
|
if item.Success == "false" {
|
|
return nil, nil
|
|
}
|
|
|
|
raw, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal item: %w", err)
|
|
}
|
|
|
|
payment := models.PSPPayment{
|
|
ParentReference: item.OriginalReference,
|
|
Reference: item.PspReference,
|
|
CreatedAt: *item.EventDate,
|
|
Type: models.PAYMENT_TYPE_PAYIN,
|
|
Amount: big.NewInt(item.Amount.Value),
|
|
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, item.Amount.Currency),
|
|
Scheme: models.PAYMENT_SCHEME_OTHER,
|
|
Status: models.PAYMENT_STATUS_REFUND_REVERSED,
|
|
DestinationAccountReference: pointer.For(item.MerchantAccountCode),
|
|
Raw: raw,
|
|
}
|
|
|
|
return &payment, nil
|
|
}
|
|
|
|
func (p *Plugin) handleRefundWithData(
|
|
item webhook.NotificationRequestItem,
|
|
) (*models.PSPPayment, error) {
|
|
if item.Success == "false" {
|
|
return nil, nil
|
|
}
|
|
|
|
raw, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal item: %w", err)
|
|
}
|
|
|
|
payment := models.PSPPayment{
|
|
ParentReference: item.OriginalReference,
|
|
Reference: item.PspReference,
|
|
CreatedAt: *item.EventDate,
|
|
Type: models.PAYMENT_TYPE_PAYIN,
|
|
Amount: big.NewInt(item.Amount.Value),
|
|
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, item.Amount.Currency),
|
|
Scheme: models.PAYMENT_SCHEME_OTHER,
|
|
Status: models.PAYMENT_STATUS_REFUNDED,
|
|
DestinationAccountReference: pointer.For(item.MerchantAccountCode),
|
|
Raw: raw,
|
|
}
|
|
|
|
return &payment, nil
|
|
}
|
|
|
|
func (p *Plugin) handlePayoutThirdparty(
|
|
item webhook.NotificationRequestItem,
|
|
) (*models.PSPPayment, error) {
|
|
raw, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
status := models.PAYMENT_STATUS_SUCCEEDED
|
|
if item.Success == "false" {
|
|
status = models.PAYMENT_STATUS_FAILED
|
|
}
|
|
|
|
payment := models.PSPPayment{
|
|
Reference: item.PspReference,
|
|
CreatedAt: *item.EventDate,
|
|
Type: models.PAYMENT_TYPE_PAYOUT,
|
|
Amount: big.NewInt(item.Amount.Value),
|
|
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, item.Amount.Currency),
|
|
Scheme: models.PAYMENT_SCHEME_OTHER,
|
|
Status: status,
|
|
SourceAccountReference: pointer.For(item.MerchantAccountCode),
|
|
Raw: raw,
|
|
}
|
|
|
|
return &payment, nil
|
|
}
|
|
|
|
func (p *Plugin) handlePayoutDecline(
|
|
item webhook.NotificationRequestItem,
|
|
) (*models.PSPPayment, error) {
|
|
if item.Success == "false" {
|
|
return nil, nil
|
|
}
|
|
|
|
raw, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
payment := models.PSPPayment{
|
|
ParentReference: item.OriginalReference,
|
|
Reference: item.PspReference,
|
|
CreatedAt: *item.EventDate,
|
|
Type: models.PAYMENT_TYPE_PAYOUT,
|
|
Amount: big.NewInt(item.Amount.Value),
|
|
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, item.Amount.Currency),
|
|
Scheme: models.PAYMENT_SCHEME_OTHER,
|
|
Status: models.PAYMENT_STATUS_FAILED,
|
|
SourceAccountReference: pointer.For(item.MerchantAccountCode),
|
|
Raw: raw,
|
|
}
|
|
|
|
return &payment, nil
|
|
}
|
|
|
|
func (p *Plugin) handlePayoutExpire(
|
|
item webhook.NotificationRequestItem,
|
|
) (*models.PSPPayment, error) {
|
|
if item.Success == "false" {
|
|
return nil, nil
|
|
}
|
|
|
|
raw, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
payment := models.PSPPayment{
|
|
ParentReference: item.OriginalReference,
|
|
Reference: item.PspReference,
|
|
CreatedAt: *item.EventDate,
|
|
Type: models.PAYMENT_TYPE_PAYOUT,
|
|
Amount: big.NewInt(item.Amount.Value),
|
|
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, item.Amount.Currency),
|
|
Scheme: models.PAYMENT_SCHEME_OTHER,
|
|
Status: models.PAYMENT_STATUS_EXPIRED,
|
|
SourceAccountReference: pointer.For(item.MerchantAccountCode),
|
|
Raw: raw,
|
|
}
|
|
|
|
return &payment, nil
|
|
}
|
|
|
|
func parseScheme(scheme string) models.PaymentScheme {
|
|
switch {
|
|
case strings.HasPrefix(scheme, "visa"):
|
|
return models.PAYMENT_SCHEME_CARD_VISA
|
|
case strings.HasPrefix(scheme, "electron"):
|
|
return models.PAYMENT_SCHEME_CARD_VISA
|
|
case strings.HasPrefix(scheme, "amex"):
|
|
return models.PAYMENT_SCHEME_CARD_AMEX
|
|
case strings.HasPrefix(scheme, "alipay"):
|
|
return models.PAYMENT_SCHEME_CARD_ALIPAY
|
|
case strings.HasPrefix(scheme, "cup"):
|
|
return models.PAYMENT_SCHEME_CARD_CUP
|
|
case strings.HasPrefix(scheme, "discover"):
|
|
return models.PAYMENT_SCHEME_CARD_DISCOVER
|
|
case strings.HasPrefix(scheme, "doku"):
|
|
return models.PAYMENT_SCHEME_DOKU
|
|
case strings.HasPrefix(scheme, "dragonpay"):
|
|
return models.PAYMENT_SCHEME_DRAGON_PAY
|
|
case strings.HasPrefix(scheme, "jcb"):
|
|
return models.PAYMENT_SCHEME_CARD_JCB
|
|
case strings.HasPrefix(scheme, "maestro"):
|
|
return models.PAYMENT_SCHEME_MAESTRO
|
|
case strings.HasPrefix(scheme, "mc"):
|
|
return models.PAYMENT_SCHEME_CARD_MASTERCARD
|
|
case strings.HasPrefix(scheme, "molpay"):
|
|
return models.PAYMENT_SCHEME_MOL_PAY
|
|
case strings.HasPrefix(scheme, "diners"):
|
|
return models.PAYMENT_SCHEME_CARD_DINERS
|
|
default:
|
|
return models.PAYMENT_SCHEME_OTHER
|
|
}
|
|
}
|