Files
c8c4fd4d58 fix(payments): EN-1345/1346/1347 payment-initiation validation & reverse workflow id (#776)
* fix(payments): EN-1345 reject non-positive payment-initiation amount

POST /v3/payment-initiations (and .../reverse) accepted a negative or zero
amount: the Amount (*big.Int) field was validated only as `required`, which
only guards against a nil pointer.

Add a gtZero custom validator (strictly positive big.Int) and apply it to the
create and reverse request amounts, so a non-positive amount is rejected with
400 VALIDATION instead of being recorded.

Confidence: high
Scope-risk: narrow

* fix(payments): EN-1346/EN-1347 shorten reverse-payout/transfer workflow id

Reversing a payment initiation or payout returned 500: the reverse-payout and
reverse-transfer Temporal WorkflowId embedded reversal.CreatedAt.String() (a
verbose, space-containing timestamp) in addition to the connector id and the
reversal id, pushing it past Temporal's WorkflowId length limit
("WorkflowId length exceeds limit.").

reversal.ID already uniquely identifies the reversal (reference + connector),
so drop the CreatedAt timestamp from both ids. The construction now matches the
proven-good create-payout/create-transfer ids.

Constraint: reverse ids must stay deterministic and unique per reversal
Rejected: Hash the whole workflow id | diverges from the readable id convention; removing the timestamp is enough to match the working create path
Confidence: high
Scope-risk: narrow
Not-tested: end-to-end reverse against a live Temporal server (covered via engine workflow-id unit assertions)

---------

Co-authored-by: fabrice guery <[email protected]>
2026-06-30 10:53:12 +00:00

83 lines
2.7 KiB
Go

package validation
import (
"fmt"
"github.com/go-playground/locales"
"github.com/go-playground/locales/en"
"github.com/go-playground/validator/v10"
ut "github.com/go-playground/universal-translator"
en_translations "github.com/go-playground/validator/v10/translations/en"
)
var (
defaultLocale locales.Translator
supportedLocales []locales.Translator
)
func init() {
defaultLocale = en.New()
supportedLocales = []locales.Translator{defaultLocale}
}
type Validator struct {
internal *validator.Validate
translator ut.Translator
}
func NewValidator() *Validator {
uni := ut.New(defaultLocale, supportedLocales...)
// TODO: non-default locale could be configured for a stack if we have non-english clients who need it
translator, _ := uni.GetTranslator(defaultLocale.Locale())
validate := validator.New(validator.WithRequiredStructEnabled())
en_translations.RegisterDefaultTranslations(validate, translator) //nolint:errcheck
registerCustomChecker("accountID", IsAccountID, "", validate, translator)
registerCustomChecker("accountType", IsAccountType, "", validate, translator)
registerCustomChecker("connectorID", IsConnectorID, "", validate, translator)
registerCustomChecker("paymentType", IsPaymentType, "", validate, translator)
registerCustomChecker("paymentScheme", IsPaymentScheme, "", validate, translator)
registerCustomChecker("paymentStatus", IsPaymentStatus, "", validate, translator)
registerCustomChecker("paymentInitiationType", IsPaymentInitiationType, "", validate, translator)
registerCustomChecker("asset", IsAsset, "", validate, translator)
registerCustomChecker("phoneNumber", IsPhoneNumber, "", validate, translator)
registerCustomChecker("email", IsEmail, "", validate, translator)
registerCustomChecker("locale", IsLocale, "", validate, translator)
registerCustomChecker("gtZero", IsPositiveBigInt, "{0} must be greater than 0", validate, translator)
return &Validator{
internal: validate,
translator: translator,
}
}
func (v *Validator) Validate(payload any) (fieldErrors validator.ValidationErrors, err error) {
rawErr := v.internal.Struct(payload)
if rawErr == nil {
return fieldErrors, nil
}
fieldErrors, ok := rawErr.(validator.ValidationErrors)
if !ok {
return fieldErrors, rawErr
}
if len(fieldErrors) > 0 {
return fieldErrors, fmt.Errorf("%s", fieldErrors[0].Translate(v.translator))
}
return fieldErrors, rawErr
}
func fieldLevelToString(fl validator.FieldLevel) (str string, err error) {
switch v := fl.Field().Interface().(type) {
case *string:
str = *fl.Field().Interface().(*string)
case string:
str = fl.Field().Interface().(string)
default:
return str, fmt.Errorf("unsupported type %T", v)
}
return str, nil
}