Bootstrap copied BFF and local accounts with public Sendico contracts (#34)

This commit is contained in:
Stephan D committed 2026-09-12 13:37:48 +02:00
1 parent 67dc5b5b1e
commit 7062672a87
556 files changed
+42291 -777

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
package merrors
var ErrAccountingInvalidPosting = Define("accounting.invalid_posting")
var ErrAccountingUnbalancedPosting = Define("accounting.unbalanced_posting")
var ErrAccountingAssetMismatch = Define("accounting.asset_mismatch")
var ErrAccountingAccountNotFound = Define("accounting.account_not_found")
var ErrAccountingAccountInactive = Define("accounting.account_inactive")
var ErrAccountingInvalidAmount = Define("accounting.invalid_amount")
var ErrAccountingInvalidIdempotency = Define("accounting.invalid_idempotency")
var ErrAccountingIdempotencyConflict = Define("accounting.idempotency_conflict")
var ErrAccountingStorageFailed = Define("accounting.storage_failed")
var ErrAccountingDuplicateKey = Define("accounting.duplicate_key")
var ErrAccountingOptimisticConflict = Define("accounting.optimistic_conflict")
var ErrAccountingInvalidStorageState = Define("accounting.invalid_storage_state")
+20
View File
@@ -0,0 +1,20 @@
package merrors
import "strings"
const ReasonAMLSourceIdempotencyConflict = "AML_SOURCE_IDEMPOTENCY_CONFLICT"
var ErrAMLSourceIdempotencyConflict = Define("aml.source_idempotency_conflict")
func AMLSourceIdempotencyConflict(msg string) error {
msg = strings.TrimSpace(msg)
if msg == "" {
msg = ErrAMLSourceIdempotencyConflict.Error()
}
return classifiedError{
base: ErrDataConflict,
code: ErrAMLSourceIdempotencyConflict,
msg: msg,
reason: ReasonAMLSourceIdempotencyConflict,
}
}
+21
View File
@@ -0,0 +1,21 @@
package merrors
import (
"errors"
"testing"
)
func TestAMLSourceIdempotencyConflictReasonRoundTrip(t *testing.T) {
err := AMLSourceIdempotencyConflict("key is bound to another source mutation")
if !errors.Is(err, ErrDataConflict) || !errors.Is(err, ErrAMLSourceIdempotencyConflict) {
t.Fatalf("AML source idempotency conflict classification mismatch: %v", err)
}
reason, ok := Reason(err)
if !ok || reason != ReasonAMLSourceIdempotencyConflict {
t.Fatalf("AML source idempotency reason mismatch: reason=%q ok=%t", reason, ok)
}
restored, ok := FromReason(reason, err.Error())
if !ok || !errors.Is(restored, ErrAMLSourceIdempotencyConflict) {
t.Fatalf("AML source idempotency reason did not round trip: %v", restored)
}
}
+21
View File
@@ -0,0 +1,21 @@
package merrors
import "strings"
const (
ReasonChainExecutionSigningUnavailable = "chain_execution.signing_unavailable"
ReasonChainExecutionUnsupportedCapability = "chain_execution.unsupported_capability"
ReasonChainExecutionInsufficientBalance = "chain_execution.insufficient_balance"
ReasonChainExecutionInsufficientNativeResources = "chain_execution.insufficient_native_resources"
ReasonChainExecutionDestinationIneligible = "chain_execution.destination_ineligible"
)
// ChainExecutionPrecondition creates a typed, reason-bearing permanent
// preparation failure. Callers can classify it with both errors.Is and Reason.
func ChainExecutionPrecondition(reason, message string) error {
return classifiedError{
base: ErrFailedPrecondition,
msg: strings.TrimSpace(message),
reason: strings.TrimSpace(reason),
}
}
@@ -0,0 +1,17 @@
package merrors
import (
"errors"
"testing"
)
func TestChainExecutionPreconditionCarriesClassAndReason(t *testing.T) {
err := ChainExecutionPrecondition(ReasonChainExecutionInsufficientBalance, "source balance is insufficient")
if !errors.Is(err, ErrFailedPrecondition) {
t.Fatal("expected failed-precondition classification")
}
reason, ok := Reason(err)
if !ok || reason != ReasonChainExecutionInsufficientBalance {
t.Fatalf("unexpected reason: %q", reason)
}
}
+48
View File
@@ -0,0 +1,48 @@
package merrors
import "strings"
type classifiedError struct {
base error
code error
msg string
reason string
publicMessage bool
argumentNames []string
}
func (e classifiedError) Error() string {
if e.publicMessage {
msg := strings.TrimSpace(e.msg)
if msg != "" {
return msg
}
if e.code != nil {
return e.code.Error()
}
if e.base != nil {
return e.base.Error()
}
return ""
}
return Wrap(e.base, e.msg).Error()
}
func (e classifiedError) Unwrap() []error {
causes := make([]error, 0, 2)
if e.base != nil {
causes = append(causes, e.base)
}
if e.code != nil {
causes = append(causes, e.code)
}
return causes
}
func (e classifiedError) Reason() string {
return strings.TrimSpace(e.reason)
}
func (e classifiedError) InvalidArgumentFields() []string {
return append([]string(nil), e.argumentNames...)
}
+44
View File
@@ -0,0 +1,44 @@
package merrors
import "strings"
const (
ReasonDocumentsReportOwned = "DOCUMENTS_REPORT_OWNED"
ReasonDocumentsQuotationExpired = "DOCUMENTS_QUOTATION_EXPIRED"
)
var ErrDocumentsReportOwned = Define("documents.report_owned")
var ErrDocumentsQuotationExpired = Define("documents.quotation_expired")
// DocumentsReportOwned reports that an active or settled payout owns a
// report, so callers must not mutate its document lifecycle.
func DocumentsReportOwned(msg string) error {
msg = strings.TrimSpace(msg)
if msg == "" {
msg = "report is owned by a payout"
}
return classifiedError{
base: ErrDataConflict,
code: ErrDocumentsReportOwned,
msg: msg,
reason: ReasonDocumentsReportOwned,
publicMessage: true,
}
}
// DocumentsQuotationExpired reports that a payout registry still references
// a quotation which can no longer be executed. A fresh quotation is required.
func DocumentsQuotationExpired(msg string) error {
msg = strings.TrimSpace(msg)
if msg == "" {
msg = "the payout quotation has expired; request a new quotation"
}
return classifiedError{
base: ErrFailedPrecondition,
code: ErrDocumentsQuotationExpired,
msg: msg,
reason: ReasonDocumentsQuotationExpired,
publicMessage: true,
}
}
+37
View File
@@ -0,0 +1,37 @@
package merrors
import (
"errors"
"testing"
)
func TestDocumentsReportOwnedReasonRoundTrip(t *testing.T) {
err := DocumentsReportOwned("report has an active payout")
if !errors.Is(err, ErrDataConflict) || !errors.Is(err, ErrDocumentsReportOwned) {
t.Fatalf("DocumentsReportOwned() classification = %v", err)
}
reason, ok := Reason(err)
if !ok || reason != ReasonDocumentsReportOwned {
t.Fatalf("Reason() = %q, %t", reason, ok)
}
restored, ok := FromReason(reason, "report has an active payout")
if !ok || !errors.Is(restored, ErrDataConflict) || !errors.Is(restored, ErrDocumentsReportOwned) {
t.Fatalf("FromReason() = %v, %t", restored, ok)
}
}
func TestDocumentsQuotationExpiredReasonRoundTrip(t *testing.T) {
err := DocumentsQuotationExpired("quotation expired")
if !errors.Is(err, ErrFailedPrecondition) || !errors.Is(err, ErrDocumentsQuotationExpired) {
t.Fatalf("DocumentsQuotationExpired() classification = %v", err)
}
reason, ok := Reason(err)
if !ok || reason != ReasonDocumentsQuotationExpired {
t.Fatalf("Reason() = %q, %t", reason, ok)
}
restored, ok := FromReason(reason, "quotation expired")
if !ok || !errors.Is(restored, ErrFailedPrecondition) ||
!errors.Is(restored, ErrDocumentsQuotationExpired) {
t.Fatalf("FromReason() = %v, %t", restored, ok)
}
}
+268
View File
@@ -0,0 +1,268 @@
package merrors
import (
"errors"
"fmt"
"strings"
)
type definedError string
func (e definedError) Error() string {
return string(e)
}
type wrappedError struct {
base error
msg string
}
// opaqueWrappedError preserves error classification and causal inspection
// without including the cause text in Error. Use it at boundaries where a
// storage or provider error can contain secrets or sensitive identifiers.
type opaqueWrappedError struct {
base error
cause error
msg string
}
func (e wrappedError) Error() string {
baseText := ""
if e.base != nil {
baseText = strings.TrimSpace(e.base.Error())
}
msg := strings.TrimSpace(e.msg)
switch {
case baseText == "":
return msg
case msg == "":
return baseText
default:
return baseText + ": " + msg
}
}
func (e wrappedError) Unwrap() error {
return e.base
}
func (e opaqueWrappedError) Error() string {
return Wrap(e.base, e.msg).Error()
}
func (e opaqueWrappedError) Unwrap() []error {
causes := make([]error, 0, 2)
if e.base != nil {
causes = append(causes, e.base)
}
if e.cause != nil {
causes = append(causes, e.cause)
}
return causes
}
func Define(msg string) error {
return definedError(msg)
}
func Message(msg string) error {
return definedError(msg)
}
func Messagef(format string, args ...any) error {
return Message(fmt.Sprintf(format, args...))
}
func Wrap(err error, msg string) error {
return wrappedError{
base: err,
msg: msg,
}
}
func Wrapf(err error, format string, args ...any) error {
return Wrap(err, fmt.Sprintf(format, args...))
}
// OpaqueWrap adds safe context while retaining the cause for errors.Is and
// errors.As. Unlike Wrap, it deliberately excludes the cause text from Error.
func OpaqueWrap(base, cause error, msg string) error {
return opaqueWrappedError{base: base, cause: cause, msg: msg}
}
var ErrNotImplemented = Define("notImplemented")
func NotImplemented(msg string) error {
return Wrap(ErrNotImplemented, msg)
}
var ErrNoData = Define("noData")
func NoData(msg string) error {
return Wrap(ErrNoData, msg)
}
func NoDataf(format string, args ...any) error {
return NoData(fmt.Sprintf(format, args...))
}
var ErrInternal = Define("internal")
func Internal(msg string) error {
return Wrap(ErrInternal, msg)
}
func Internalf(format string, args ...any) error {
return Internal(fmt.Sprintf(format, args...))
}
var ErrInvalidArg = Define("invalidArgError")
func InvalidArgument(msg string, argumentNames ...string) error {
return Wrap(ErrInvalidArg, invalidArgumentMessage(msg, argumentNames...))
}
func InvalidArgumentf(format string, args ...any) error {
return InvalidArgument(fmt.Sprintf(format, args...))
}
func InvalidArgumentWithNamesf(argumentNames []string, format string, args ...any) error {
return InvalidArgument(fmt.Sprintf(format, args...), argumentNames...)
}
var ErrDataConflict = Define("dataConflict")
func DataConflict(msg string) error {
return Wrap(ErrDataConflict, msg)
}
func DataConflictf(format string, args ...any) error {
return DataConflict(fmt.Sprintf(format, args...))
}
func DataConflictWrap(err error, msg string) error {
return wrapError(ErrDataConflict, msg, err)
}
func DataConflictWrapf(err error, format string, args ...any) error {
return DataConflictWrap(err, fmt.Sprintf(format, args...))
}
var ErrFailedPrecondition = Define("failedPrecondition")
func FailedPrecondition(msg string) error {
return Wrap(ErrFailedPrecondition, msg)
}
func FailedPreconditionf(format string, args ...any) error {
return FailedPrecondition(fmt.Sprintf(format, args...))
}
func FailedPreconditionWrap(err error, msg string) error {
return wrapError(ErrFailedPrecondition, msg, err)
}
func FailedPreconditionWrapf(err error, format string, args ...any) error {
return FailedPreconditionWrap(err, fmt.Sprintf(format, args...))
}
var ErrServiceUnavailable = Define("serviceUnavailable")
func ServiceUnavailable(msg string) error {
return Wrap(ErrServiceUnavailable, msg)
}
func ServiceUnavailablef(format string, args ...any) error {
return ServiceUnavailable(fmt.Sprintf(format, args...))
}
func ServiceUnavailableWrap(err error, msg string) error {
return wrapError(ErrServiceUnavailable, msg, err)
}
func ServiceUnavailableWrapf(err error, format string, args ...any) error {
return ServiceUnavailableWrap(err, fmt.Sprintf(format, args...))
}
type objectRef interface {
IsZero() bool
Hex() string
}
var ErrAccessDenied = Define("accessDenied")
func PermissionDenied(msg string) error {
return Wrap(ErrAccessDenied, msg)
}
func AccessDenied(object, action string, ref objectRef) error {
if ref != nil && !ref.IsZero() {
return Wrap(ErrAccessDenied, fmt.Sprintf("cannot %s object of type '%s' with ID '%s'", action, object, ref.Hex()))
}
return Wrap(ErrAccessDenied, fmt.Sprintf("cannot %s object of type '%s'", action, object))
}
var ErrInvalidDataType = Define("invalidDataType")
func InvalidDataType(msg string) error {
return Wrap(ErrInvalidDataType, msg)
}
func InvalidDataTypef(format string, args ...any) error {
return InvalidDataType(fmt.Sprintf(format, args...))
}
var ErrUnauthorized = Define("unathorized")
func Unauthorized(msg string) error {
return Wrap(ErrUnauthorized, msg)
}
func Unauthorizedf(format string, args ...any) error {
return Unauthorized(fmt.Sprintf(format, args...))
}
var ErrNoMessagingTopic = Define("messagingTopicError")
func NoMessagingTopic(topic string) error {
return Wrapf(ErrNoMessagingTopic, "messaging topic '%s' not found", topic)
}
func InvalidArgumentWrap(err error, msg string, argumentNames ...string) error {
return wrapError(ErrInvalidArg, invalidArgumentMessage(msg, argumentNames...), err)
}
func InvalidArgumentWrapf(err error, format string, args ...any) error {
return InvalidArgumentWrap(err, fmt.Sprintf(format, args...))
}
func InternalWrap(err error, msg string) error {
return wrapError(ErrInternal, msg, err)
}
func InternalWrapf(err error, format string, args ...any) error {
return InternalWrap(err, fmt.Sprintf(format, args...))
}
func InternalWrapAll(msg string, errs ...error) error {
return wrapErrors(ErrInternal, msg, errs...)
}
func wrapError(base error, msg string, err error) error {
return wrapErrors(base, msg, err)
}
func wrapErrors(base error, msg string, errs ...error) error {
baseErr := Wrap(base, msg)
causes := make([]error, 0, len(errs))
for _, err := range errs {
if err != nil {
causes = append(causes, err)
}
}
if len(causes) == 0 {
return baseErr
}
return errors.Join(baseErr, errors.Join(causes...))
}
+125
View File
@@ -0,0 +1,125 @@
package merrors
import (
"errors"
"strings"
"testing"
)
type testReasonedError struct {
error
reason string
}
func (e testReasonedError) Reason() string {
return e.reason
}
func TestReasonUsesReasonedError(t *testing.T) {
err := testReasonedError{
error: InvalidArgument("value is invalid"),
reason: "amount_below_minimum",
}
reason, ok := Reason(err)
if !ok {
t.Fatalf("expected reason")
}
if reason != "amount_below_minimum" {
t.Fatalf("reason mismatch: got=%q", reason)
}
}
func TestInvalidArgumentSupportsBrokenArgumentName(t *testing.T) {
t.Run("without argument name keeps old behavior", func(t *testing.T) {
err := InvalidArgument("value is missing")
expected := "invalidArgError: value is missing"
if err.Error() != expected {
t.Fatalf("unexpected error message: %s", err)
}
if !errors.Is(err, ErrInvalidArg) {
t.Fatalf("error should wrap ErrInvalidArg")
}
})
t.Run("single argument name", func(t *testing.T) {
err := InvalidArgument("value is missing", "bot_token_env")
expected := `invalidArgError: broken argument "bot_token_env": value is missing`
if err.Error() != expected {
t.Fatalf("unexpected error message: %s", err)
}
})
t.Run("multiple argument names", func(t *testing.T) {
err := InvalidArgument("value mismatch", "bot_token_env", "chat_id_env", " ")
expected := `invalidArgError: broken arguments "bot_token_env", "chat_id_env": value mismatch`
if err.Error() != expected {
t.Fatalf("unexpected error message: %s", err)
}
})
}
func TestInvalidArgumentWithNamesfSupportsBrokenArgumentName(t *testing.T) {
err := InvalidArgumentWithNamesf([]string{"gateway.supported_assets.currency"}, "asset[%d] %s", 2, "is invalid")
expected := `invalidArgError: broken argument "gateway.supported_assets.currency": asset[2] is invalid`
if err.Error() != expected {
t.Fatalf("unexpected error message: %s", err)
}
if !errors.Is(err, ErrInvalidArg) {
t.Fatalf("error should wrap ErrInvalidArg")
}
}
func TestInvalidArgumentWrapSupportsBrokenArgumentName(t *testing.T) {
base := Define("root cause")
err := InvalidArgumentWrap(base, "value is missing", "bot_token_env")
if !strings.Contains(err.Error(), `invalidArgError: broken argument "bot_token_env": value is missing`) {
t.Fatalf("wrapped error should include broken argument name: %s", err)
}
if !errors.Is(err, ErrInvalidArg) || !errors.Is(err, base) {
t.Fatalf("wrapped error should preserve all error layers")
}
}
func TestInternalWrapAllPreservesAllCauses(t *testing.T) {
first := Define("first cause")
second := Define("second cause")
err := InternalWrapAll("combined failure", first, nil, second)
if !strings.Contains(err.Error(), "internal: combined failure") {
t.Fatalf("wrapped error should include context: %s", err)
}
if !errors.Is(err, ErrInternal) || !errors.Is(err, first) || !errors.Is(err, second) {
t.Fatalf("wrapped error should preserve all error layers")
}
}
func TestOpaqueWrapPreservesCauseWithoutExposingItsText(t *testing.T) {
cause := DataConflict("restricted storage key")
err := OpaqueWrap(ErrInternal, cause, "safe persistence failure")
if got, want := err.Error(), "internal: safe persistence failure"; got != want {
t.Fatalf("opaque error mismatch: got=%q want=%q", got, want)
}
if !errors.Is(err, ErrInternal) || !errors.Is(err, ErrDataConflict) {
t.Fatalf("opaque error did not preserve classifications: %v", err)
}
}
func TestOperationalWrapsPreserveClassificationAndCause(t *testing.T) {
cause := Define("dependency failure")
tests := []struct {
name string
err error
kind error
}{
{name: "failed precondition", err: FailedPreconditionWrap(cause, "AA capability unavailable"), kind: ErrFailedPrecondition},
{name: "service unavailable", err: ServiceUnavailableWrap(cause, "Arbitrum RPC unavailable"), kind: ErrServiceUnavailable},
{name: "data conflict", err: DataConflictWrap(cause, "managed wallet replay differs"), kind: ErrDataConflict},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if !errors.Is(tt.err, tt.kind) || !errors.Is(tt.err, cause) {
t.Fatalf("wrapped error should preserve classification and cause: %s", tt.err)
}
})
}
}
+44
View File
@@ -0,0 +1,44 @@
package merrors
import (
"fmt"
"strings"
)
// InvalidArgumentClassified returns an invalid-argument error with a stable
// machine reason and structured field names for transport adapters.
func InvalidArgumentClassified(reason, msg string, argumentNames ...string) error {
return classifiedError{
base: ErrInvalidArg,
msg: invalidArgumentMessage(msg, argumentNames...),
reason: strings.TrimSpace(reason),
argumentNames: normalizeArgumentNames(argumentNames),
}
}
func invalidArgumentMessage(msg string, argumentNames ...string) string {
normalizedNames := normalizeArgumentNames(argumentNames)
names := make([]string, 0, len(normalizedNames))
for _, name := range normalizedNames {
names = append(names, fmt.Sprintf("%q", name))
}
if len(names) == 0 {
return msg
}
prefix := "broken argument"
if len(names) > 1 {
prefix = "broken arguments"
}
return fmt.Sprintf("%s %s: %s", prefix, strings.Join(names, ", "), msg)
}
func normalizeArgumentNames(argumentNames []string) []string {
names := make([]string, 0, len(argumentNames))
for _, name := range argumentNames {
if name = strings.TrimSpace(name); name != "" {
names = append(names, name)
}
}
return names
}
+65
View File
@@ -0,0 +1,65 @@
package merrors
import "strings"
const (
ReasonLedgerIdempotencyConflict = "LEDGER_IDEMPOTENCY_CONFLICT"
ReasonLedgerCommandIdentityMismatch = "LEDGER_COMMAND_IDENTITY_MISMATCH"
ReasonLedgerAccessViolation = "LEDGER_ACCESS_VIOLATION"
ReasonAccountNotEligible = "ACCOUNT_NOT_ELIGIBLE"
)
var ErrLedgerIdempotencyConflict = Define("ledger.idempotency_conflict")
var ErrLedgerCommandIdentityMismatch = Define("ledger.command_identity_mismatch")
var ErrLedgerAccessViolation = Define("ledger.access_violation")
var ErrAccountNotEligible = Define("account_not_eligible")
func LedgerIdempotencyConflict(msg string) error {
return classifiedError{
base: ErrDataConflict,
code: ErrLedgerIdempotencyConflict,
msg: ledgerErrorMessage(msg, ErrLedgerIdempotencyConflict),
reason: ReasonLedgerIdempotencyConflict,
}
}
func LedgerCommandIdentityMismatch(msg string) error {
return classifiedError{
base: ErrDataConflict,
code: ErrLedgerCommandIdentityMismatch,
msg: ledgerErrorMessage(msg, ErrLedgerCommandIdentityMismatch),
reason: ReasonLedgerCommandIdentityMismatch,
}
}
func LedgerAccessViolation(msg string) error {
return classifiedError{
base: ErrFailedPrecondition,
code: ErrLedgerAccessViolation,
msg: ledgerErrorMessage(msg, ErrLedgerAccessViolation),
reason: ReasonLedgerAccessViolation,
}
}
func AccountNotEligible(msg string) error {
return classifiedError{
base: ErrFailedPrecondition,
code: ErrAccountNotEligible,
msg: ledgerErrorMessage(msg, ErrAccountNotEligible),
reason: ReasonAccountNotEligible,
}
}
func ledgerErrorMessage(msg string, fallback error) string {
msg = strings.TrimSpace(msg)
if msg != "" {
return msg
}
if fallback == nil {
return ""
}
return fallback.Error()
}
+48
View File
@@ -0,0 +1,48 @@
package merrors
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
)
func TestLedgerReasonRoundTrip(t *testing.T) {
err := LedgerIdempotencyConflict("conflicting command")
reason, ok := Reason(err)
require.True(t, ok)
require.Equal(t, ReasonLedgerIdempotencyConflict, reason)
restored, ok := FromReason(reason, "conflicting command")
require.True(t, ok)
require.True(t, errors.Is(restored, ErrLedgerIdempotencyConflict))
require.True(t, errors.Is(restored, ErrDataConflict))
}
func TestAccountNotEligibleReasonRoundTrip(t *testing.T) {
err := AccountNotEligible("account is frozen")
reason, ok := Reason(err)
require.True(t, ok)
require.Equal(t, ReasonAccountNotEligible, reason)
require.True(t, errors.Is(err, ErrAccountNotEligible))
require.True(t, errors.Is(err, ErrFailedPrecondition))
restored, ok := FromReason(reason, "account is frozen")
require.True(t, ok)
require.True(t, errors.Is(restored, ErrAccountNotEligible))
require.True(t, errors.Is(restored, ErrFailedPrecondition))
}
func TestLedgerAccessViolationReasonRoundTrip(t *testing.T) {
err := LedgerAccessViolation("from_account: account does not belong to organization")
reason, ok := Reason(err)
require.True(t, ok)
require.Equal(t, ReasonLedgerAccessViolation, reason)
require.True(t, errors.Is(err, ErrLedgerAccessViolation))
require.True(t, errors.Is(err, ErrFailedPrecondition))
restored, ok := FromReason(reason, "from_account: account does not belong to organization")
require.True(t, ok)
require.True(t, errors.Is(restored, ErrLedgerAccessViolation))
require.True(t, errors.Is(restored, ErrFailedPrecondition))
}
+19
View File
@@ -0,0 +1,19 @@
package merrors
var ErrLimitsRuntimeInvalidRecord = Define("limits.runtime.invalid_record")
var ErrLimitsRuntimeCASConflict = Define("limits.runtime.cas_conflict")
var ErrLimitsRuntimeDuplicateStateKey = Define("limits.runtime.duplicate_state_key")
var ErrLimitsRuntimeDuplicateStateRef = Define("limits.runtime.duplicate_state_ref")
var ErrLimitsRuntimeDuplicateReservationRef = Define("limits.runtime.duplicate_reservation_ref")
var ErrLimitsRuntimeDuplicateReservationOperation = Define("limits.runtime.duplicate_reservation_operation")
var ErrLimitsRuntimeIdentityConflict = Define("limits.runtime.identity_conflict")
var ErrLimitsRuntimeDataConflict = Define("limits.runtime.data_conflict")
var ErrLimitsRuntimeRepositoryRequired = Define("limits.runtime.repository_required")
+66
View File
@@ -0,0 +1,66 @@
package merrors
import "strings"
// Reasons for organization provisioning outcomes. They travel to callers in
// gRPC error details so every outcome stays distinguishable regardless of the
// status code it shares with others.
const (
ReasonOrganizationProvisioningInProgress = "organization.provisioning_in_progress"
ReasonOrganizationAlreadyActive = "organization.already_active"
ReasonOrganizationProvisioningBlocked = "organization.provisioning_blocked"
ReasonOrganizationOwnerMismatch = "organization.owner_mismatch"
ReasonOrganizationNotOperatorProvisioned = "organization.not_operator_provisioned"
)
// ErrOrganizationProvisioningInProgress means another attempt currently holds
// the provisioning lease of the organization.
var ErrOrganizationProvisioningInProgress = Define(ReasonOrganizationProvisioningInProgress)
// ErrOrganizationAlreadyActive means the organization finished provisioning already.
var ErrOrganizationAlreadyActive = Define(ReasonOrganizationAlreadyActive)
// ErrOrganizationProvisioningBlocked means a failed signup compensated the
// organization and it requires manual recovery before it can be activated.
var ErrOrganizationProvisioningBlocked = Define(ReasonOrganizationProvisioningBlocked)
// ErrOrganizationOwnerMismatch means the organization is being provisioned for
// a different account than the one supplied.
var ErrOrganizationOwnerMismatch = Define(ReasonOrganizationOwnerMismatch)
// ErrOrganizationNotOperatorProvisioned means the organization was created through signup:
// organizations created through general signup are never finished by someone in backoffice.
var ErrOrganizationNotOperatorProvisioned = Define(ReasonOrganizationNotOperatorProvisioned)
func OrganizationProvisioningInProgress(msg string) error {
return organizationError(ErrOrganizationProvisioningInProgress, ReasonOrganizationProvisioningInProgress, msg)
}
func OrganizationAlreadyActive(msg string) error {
return organizationError(ErrOrganizationAlreadyActive, ReasonOrganizationAlreadyActive, msg)
}
func OrganizationProvisioningBlocked(msg string) error {
return organizationError(ErrOrganizationProvisioningBlocked, ReasonOrganizationProvisioningBlocked, msg)
}
func OrganizationOwnerMismatch(msg string) error {
return organizationError(ErrOrganizationOwnerMismatch, ReasonOrganizationOwnerMismatch, msg)
}
func OrganizationNotOperatorProvisioned(msg string) error {
return organizationError(ErrOrganizationNotOperatorProvisioned, ReasonOrganizationNotOperatorProvisioned, msg)
}
func organizationError(code error, reason, msg string) error {
msg = strings.TrimSpace(msg)
if msg == "" && code != nil {
msg = code.Error()
}
return classifiedError{
base: ErrDataConflict,
code: code,
msg: msg,
reason: reason,
}
}
+9
View File
@@ -0,0 +1,9 @@
package merrors
var ErrPaymasterPaused = Define("paymaster_paused")
var ErrControlWalletInsufficientNativeBalance = Define("control_wallet_insufficient_native_balance")
var ErrPaymasterControlDurableWalletFailure = Define("paymaster_control_durable_wallet_failure")
var ErrPaymasterControlKeyActivityMismatch = Define("paymaster_control_key_activity_mismatch")
+47
View File
@@ -0,0 +1,47 @@
package merrors
import "strings"
const ReasonPaymentExecutionNotAccepted = "PAYMENT_EXECUTION_NOT_ACCEPTED"
var ErrPaymentExecutionNotAccepted = Define("payment.execution_not_accepted")
type paymentExecutionNotAcceptedError struct {
classifiedError
cause error
}
func (e paymentExecutionNotAcceptedError) Unwrap() []error {
causes := e.classifiedError.Unwrap()
if e.cause != nil {
causes = append(causes, e.cause)
}
return causes
}
// PaymentExecutionNotAccepted reports that the orchestrator proved an
// execution request did not create or reuse any payment. Callers may release
// request-side reservations only for this outcome; ordinary transport and
// internal errors have an ambiguous ownership result.
func PaymentExecutionNotAccepted(msg string) error {
return PaymentExecutionNotAcceptedWrap(nil, msg)
}
// PaymentExecutionNotAcceptedWrap adds the definitive rejection
// classification while retaining the underlying cause for diagnostics.
func PaymentExecutionNotAcceptedWrap(cause error, msg string) error {
msg = strings.TrimSpace(msg)
if msg == "" {
msg = "payment execution was not accepted before a payment was created"
}
return paymentExecutionNotAcceptedError{
classifiedError: classifiedError{
base: ErrFailedPrecondition,
code: ErrPaymentExecutionNotAccepted,
msg: msg,
reason: ReasonPaymentExecutionNotAccepted,
publicMessage: true,
},
cause: cause,
}
}
@@ -0,0 +1,26 @@
package merrors
import (
"errors"
"testing"
)
func TestPaymentExecutionNotAcceptedPreservesClassificationAndCause(t *testing.T) {
cause := ServiceUnavailable("orchestrator unavailable")
err := PaymentExecutionNotAcceptedWrap(cause, "execution was rejected")
if !errors.Is(err, ErrFailedPrecondition) ||
!errors.Is(err, ErrPaymentExecutionNotAccepted) ||
!errors.Is(err, cause) {
t.Fatalf("PaymentExecutionNotAcceptedWrap() classification = %v", err)
}
reason, ok := Reason(err)
if !ok || reason != ReasonPaymentExecutionNotAccepted {
t.Fatalf("PaymentExecutionNotAcceptedWrap() reason = %q, %t", reason, ok)
}
restored, ok := FromReason(reason, err.Error())
if !ok || !errors.Is(restored, ErrPaymentExecutionNotAccepted) ||
!errors.Is(restored, ErrFailedPrecondition) {
t.Fatalf("FromReason() = %v, %t", restored, ok)
}
}
+32
View File
@@ -0,0 +1,32 @@
package merrors
const (
QuotationFeeConfigurationMissingCode = "quotation_fee_configuration_missing"
QuotationFeeConfigurationMissingDetails = "quotation fee configuration is missing"
QuotationReplayMissingCode = "quotation_replay_missing"
QuotationReplayMissingDetails = "the expected payment quotation is no longer available"
)
var ErrQuotationFeeConfigurationMissing = Define(QuotationFeeConfigurationMissingCode)
var ErrQuotationReplayMissing = Define(QuotationReplayMissingCode)
func QuotationFeeConfigurationMissing() error {
return classifiedError{
base: ErrFailedPrecondition,
code: ErrQuotationFeeConfigurationMissing,
msg: QuotationFeeConfigurationMissingDetails,
reason: QuotationFeeConfigurationMissingCode,
publicMessage: true,
}
}
func QuotationReplayMissing() error {
return classifiedError{
base: ErrNoData,
code: ErrQuotationReplayMissing,
msg: QuotationReplayMissingDetails,
reason: QuotationReplayMissingCode,
publicMessage: true,
}
}
+44
View File
@@ -0,0 +1,44 @@
package merrors
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
)
func TestQuotationFeeConfigurationMissingReasonRoundTrip(t *testing.T) {
err := QuotationFeeConfigurationMissing()
require.True(t, errors.Is(err, ErrFailedPrecondition))
require.True(t, errors.Is(err, ErrQuotationFeeConfigurationMissing))
reason, ok := Reason(err)
require.True(t, ok)
require.Equal(t, QuotationFeeConfigurationMissingCode, reason)
require.Equal(t, QuotationFeeConfigurationMissingDetails, err.Error())
restored, ok := FromReason(reason, "ignored message")
require.True(t, ok)
require.True(t, errors.Is(restored, ErrFailedPrecondition))
require.True(t, errors.Is(restored, ErrQuotationFeeConfigurationMissing))
require.Equal(t, QuotationFeeConfigurationMissingDetails, restored.Error())
}
func TestQuotationReplayMissingReasonRoundTrip(t *testing.T) {
err := QuotationReplayMissing()
require.True(t, errors.Is(err, ErrNoData))
require.True(t, errors.Is(err, ErrQuotationReplayMissing))
reason, ok := Reason(err)
require.True(t, ok)
require.Equal(t, QuotationReplayMissingCode, reason)
require.Equal(t, QuotationReplayMissingDetails, err.Error())
restored, ok := FromReason(reason, "ignored message")
require.True(t, ok)
require.True(t, errors.Is(restored, ErrNoData))
require.True(t, errors.Is(restored, ErrQuotationReplayMissing))
require.Equal(t, QuotationReplayMissingDetails, restored.Error())
}
+105
View File
@@ -0,0 +1,105 @@
package merrors
import (
"errors"
"strings"
)
func Reason(err error) (string, bool) {
var reasoned interface {
Reason() string
}
if errors.As(err, &reasoned) {
reason := strings.TrimSpace(reasoned.Reason())
if reason != "" {
return reason, true
}
}
switch {
case errors.Is(err, ErrAMLSourceIdempotencyConflict):
return ReasonAMLSourceIdempotencyConflict, true
case errors.Is(err, ErrLedgerIdempotencyConflict):
return ReasonLedgerIdempotencyConflict, true
case errors.Is(err, ErrLedgerCommandIdentityMismatch):
return ReasonLedgerCommandIdentityMismatch, true
case errors.Is(err, ErrLedgerAccessViolation):
return ReasonLedgerAccessViolation, true
case errors.Is(err, ErrAccountNotEligible):
return ReasonAccountNotEligible, true
case errors.Is(err, ErrQuotationFeeConfigurationMissing):
return QuotationFeeConfigurationMissingCode, true
case errors.Is(err, ErrQuotationReplayMissing):
return QuotationReplayMissingCode, true
default:
return "", false
}
}
func FromReason(reason string, msg string) (error, bool) {
switch strings.TrimSpace(reason) {
case ReasonRouteNotFound:
return RouteNotFound(msg), true
case ReasonRouteAmbiguous:
return RouteAmbiguous(msg), true
case ReasonRouteDisabled:
return RouteDisabled(msg), true
case ReasonRouteInvalidSource:
return RouteInvalidSource(msg), true
case ReasonRouteDestinationNotFound:
return RouteDestinationNotFound(msg), true
case ReasonRouteDestinationInactive:
return RouteDestinationInactive(msg), true
case ReasonRouteDestinationCurrencyMismatch:
return RouteDestinationCurrencyMismatch(msg), true
case ReasonRouteDestinationOrgMismatch:
return RouteDestinationOrgMismatch(msg), true
case ReasonRouteDestinationAmbiguous:
return RouteDestinationAmbiguous(msg), true
case ReasonRouteDestinationOwnerMismatch:
return RouteDestinationOwnerMismatch(msg), true
case ReasonRouteActiveSourceConflict:
return RouteActiveSourceConflict(msg), true
case ReasonRouteDestinationTypeMismatch:
return RouteDestinationTypeMismatch(msg), true
case ReasonRouteDestinationRoleMismatch:
return RouteDestinationRoleMismatch(msg), true
case ReasonRouteIdempotencyConflict:
return RouteIdempotencyConflict(msg), true
case ReasonRouteVersionConflict:
return RouteVersionConflict(msg), true
case ReasonRouteInvalidState:
return RouteInvalidState(msg), true
case ReasonAMLSourceIdempotencyConflict:
return AMLSourceIdempotencyConflict(msg), true
case ReasonLedgerIdempotencyConflict:
return LedgerIdempotencyConflict(msg), true
case ReasonLedgerCommandIdentityMismatch:
return LedgerCommandIdentityMismatch(msg), true
case ReasonLedgerAccessViolation:
return LedgerAccessViolation(msg), true
case ReasonAccountNotEligible:
return AccountNotEligible(msg), true
case ReasonDocumentsReportOwned:
return DocumentsReportOwned(msg), true
case ReasonDocumentsQuotationExpired:
return DocumentsQuotationExpired(msg), true
case ReasonPaymentExecutionNotAccepted:
return PaymentExecutionNotAccepted(msg), true
case QuotationFeeConfigurationMissingCode:
return QuotationFeeConfigurationMissing(), true
case QuotationReplayMissingCode:
return QuotationReplayMissing(), true
case ReasonOrganizationProvisioningInProgress:
return OrganizationProvisioningInProgress(msg), true
case ReasonOrganizationAlreadyActive:
return OrganizationAlreadyActive(msg), true
case ReasonOrganizationProvisioningBlocked:
return OrganizationProvisioningBlocked(msg), true
case ReasonOrganizationOwnerMismatch:
return OrganizationOwnerMismatch(msg), true
case ReasonOrganizationNotOperatorProvisioned:
return OrganizationNotOperatorProvisioned(msg), true
default:
return nil, false
}
}
+127
View File
@@ -0,0 +1,127 @@
package merrors
import "strings"
const routeErrorCodePrefix = "ROUTE_ERROR_CODE_"
type routeMetadataError struct {
classifiedError
routeCode string
}
func (e routeMetadataError) ErrorInfoMetadata() map[string]string {
return map[string]string{"route_error_code": e.routeCode}
}
const (
ReasonRouteNotFound = "ROUTE_NOT_FOUND"
ReasonRouteAmbiguous = "ROUTE_AMBIGUOUS"
ReasonRouteDisabled = "ROUTE_DISABLED"
ReasonRouteInvalidSource = "ROUTE_INVALID_SOURCE"
ReasonRouteDestinationNotFound = "ROUTE_DESTINATION_NOT_FOUND"
ReasonRouteDestinationInactive = "ROUTE_DESTINATION_INACTIVE"
ReasonRouteDestinationCurrencyMismatch = "ROUTE_DESTINATION_CURRENCY_MISMATCH"
ReasonRouteDestinationOrgMismatch = "ROUTE_DESTINATION_ORG_MISMATCH"
ReasonRouteDestinationAmbiguous = "ROUTE_DESTINATION_AMBIGUOUS"
ReasonRouteDestinationOwnerMismatch = "ROUTE_DESTINATION_OWNER_MISMATCH"
ReasonRouteActiveSourceConflict = "ROUTE_ACTIVE_SOURCE_CONFLICT"
ReasonRouteDestinationTypeMismatch = "ROUTE_DESTINATION_TYPE_MISMATCH"
ReasonRouteDestinationRoleMismatch = "ROUTE_DESTINATION_ROLE_MISMATCH"
ReasonRouteIdempotencyConflict = "ROUTE_IDEMPOTENCY_CONFLICT"
ReasonRouteVersionConflict = "ROUTE_VERSION_CONFLICT"
ReasonRouteInvalidState = "ROUTE_INVALID_STATE"
)
var (
ErrRouteNotFound = Define("route.not_found")
ErrRouteAmbiguous = Define("route.ambiguous")
ErrRouteDisabled = Define("route.disabled")
ErrRouteInvalidSource = Define("route.invalid_source")
ErrRouteDestinationNotFound = Define("route.destination_not_found")
ErrRouteDestinationInactive = Define("route.destination_inactive")
ErrRouteDestinationCurrencyMismatch = Define("route.destination_currency_mismatch")
ErrRouteDestinationOrgMismatch = Define("route.destination_org_mismatch")
ErrRouteDestinationAmbiguous = Define("route.destination_ambiguous")
ErrRouteDestinationOwnerMismatch = Define("route.destination_owner_mismatch")
ErrRouteActiveSourceConflict = Define("route.active_source_conflict")
ErrRouteDestinationTypeMismatch = Define("route.destination_type_mismatch")
ErrRouteDestinationRoleMismatch = Define("route.destination_role_mismatch")
ErrRouteIdempotencyConflict = Define("route.idempotency_conflict")
ErrRouteVersionConflict = Define("route.version_conflict")
ErrRouteInvalidState = Define("route.invalid_state")
)
func RouteNotFound(msg string) error {
return newRouteClassifiedError(ErrNoData, ErrRouteNotFound, ReasonRouteNotFound, msg)
}
func RouteAmbiguous(msg string) error {
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteAmbiguous, ReasonRouteAmbiguous, msg)
}
func RouteDisabled(msg string) error {
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteDisabled, ReasonRouteDisabled, msg)
}
func RouteInvalidSource(msg string) error {
return newRouteClassifiedError(ErrInvalidArg, ErrRouteInvalidSource, ReasonRouteInvalidSource, msg)
}
func RouteDestinationNotFound(msg string) error {
return newRouteClassifiedError(ErrNoData, ErrRouteDestinationNotFound, ReasonRouteDestinationNotFound, msg)
}
func RouteDestinationInactive(msg string) error {
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteDestinationInactive, ReasonRouteDestinationInactive, msg)
}
func RouteDestinationCurrencyMismatch(msg string) error {
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteDestinationCurrencyMismatch, ReasonRouteDestinationCurrencyMismatch, msg)
}
func RouteDestinationOrgMismatch(msg string) error {
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteDestinationOrgMismatch, ReasonRouteDestinationOrgMismatch, msg)
}
func RouteDestinationAmbiguous(msg string) error {
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteDestinationAmbiguous, ReasonRouteDestinationAmbiguous, msg)
}
func RouteDestinationOwnerMismatch(msg string) error {
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteDestinationOwnerMismatch, ReasonRouteDestinationOwnerMismatch, msg)
}
func RouteActiveSourceConflict(msg string) error {
return newRouteClassifiedError(ErrDataConflict, ErrRouteActiveSourceConflict, ReasonRouteActiveSourceConflict, msg)
}
func RouteDestinationTypeMismatch(msg string) error {
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteDestinationTypeMismatch, ReasonRouteDestinationTypeMismatch, msg)
}
func RouteDestinationRoleMismatch(msg string) error {
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteDestinationRoleMismatch, ReasonRouteDestinationRoleMismatch, msg)
}
func RouteIdempotencyConflict(msg string) error {
return newRouteClassifiedError(ErrDataConflict, ErrRouteIdempotencyConflict, ReasonRouteIdempotencyConflict, msg)
}
func RouteVersionConflict(msg string) error {
return newRouteClassifiedError(ErrDataConflict, ErrRouteVersionConflict, ReasonRouteVersionConflict, msg)
}
func RouteInvalidState(msg string) error {
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteInvalidState, ReasonRouteInvalidState, msg)
}
func newRouteClassifiedError(base, code error, reason, msg string) error {
msg = strings.TrimSpace(msg)
if msg == "" && code != nil {
msg = code.Error()
}
return routeMetadataError{
classifiedError: classifiedError{base: base, code: code, msg: msg, reason: reason, publicMessage: true},
routeCode: routeErrorCodePrefix + reason,
}
}
+52
View File
@@ -0,0 +1,52 @@
package merrors
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
)
func TestRouteReasonsRoundTrip(t *testing.T) {
tests := []struct {
name string
create func(string) error
code error
base error
reason string
}{
{name: "not found", create: RouteNotFound, code: ErrRouteNotFound, base: ErrNoData, reason: ReasonRouteNotFound},
{name: "ambiguous", create: RouteAmbiguous, code: ErrRouteAmbiguous, base: ErrFailedPrecondition, reason: ReasonRouteAmbiguous},
{name: "disabled", create: RouteDisabled, code: ErrRouteDisabled, base: ErrFailedPrecondition, reason: ReasonRouteDisabled},
{name: "invalid source", create: RouteInvalidSource, code: ErrRouteInvalidSource, base: ErrInvalidArg, reason: ReasonRouteInvalidSource},
{name: "destination not found", create: RouteDestinationNotFound, code: ErrRouteDestinationNotFound, base: ErrNoData, reason: ReasonRouteDestinationNotFound},
{name: "destination inactive", create: RouteDestinationInactive, code: ErrRouteDestinationInactive, base: ErrFailedPrecondition, reason: ReasonRouteDestinationInactive},
{name: "destination currency mismatch", create: RouteDestinationCurrencyMismatch, code: ErrRouteDestinationCurrencyMismatch, base: ErrFailedPrecondition, reason: ReasonRouteDestinationCurrencyMismatch},
{name: "destination organization mismatch", create: RouteDestinationOrgMismatch, code: ErrRouteDestinationOrgMismatch, base: ErrFailedPrecondition, reason: ReasonRouteDestinationOrgMismatch},
{name: "destination ambiguous", create: RouteDestinationAmbiguous, code: ErrRouteDestinationAmbiguous, base: ErrFailedPrecondition, reason: ReasonRouteDestinationAmbiguous},
{name: "destination owner mismatch", create: RouteDestinationOwnerMismatch, code: ErrRouteDestinationOwnerMismatch, base: ErrFailedPrecondition, reason: ReasonRouteDestinationOwnerMismatch},
{name: "active source conflict", create: RouteActiveSourceConflict, code: ErrRouteActiveSourceConflict, base: ErrDataConflict, reason: ReasonRouteActiveSourceConflict},
{name: "destination type mismatch", create: RouteDestinationTypeMismatch, code: ErrRouteDestinationTypeMismatch, base: ErrFailedPrecondition, reason: ReasonRouteDestinationTypeMismatch},
{name: "destination role mismatch", create: RouteDestinationRoleMismatch, code: ErrRouteDestinationRoleMismatch, base: ErrFailedPrecondition, reason: ReasonRouteDestinationRoleMismatch},
{name: "idempotency conflict", create: RouteIdempotencyConflict, code: ErrRouteIdempotencyConflict, base: ErrDataConflict, reason: ReasonRouteIdempotencyConflict},
{name: "version conflict", create: RouteVersionConflict, code: ErrRouteVersionConflict, base: ErrDataConflict, reason: ReasonRouteVersionConflict},
{name: "invalid state", create: RouteInvalidState, code: ErrRouteInvalidState, base: ErrFailedPrecondition, reason: ReasonRouteInvalidState},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.create("route error")
require.Equal(t, "route error", err.Error())
reason, ok := Reason(err)
require.True(t, ok)
require.Equal(t, tt.reason, reason)
require.True(t, errors.Is(err, tt.code))
require.True(t, errors.Is(err, tt.base))
restored, ok := FromReason(reason, "route error")
require.True(t, ok)
require.True(t, errors.Is(restored, tt.code))
require.True(t, errors.Is(restored, tt.base))
})
}
}
@@ -0,0 +1,78 @@
package merrors
import (
"errors"
"strings"
)
const (
SignupEmailPolicyCode = "signup_email_rejected"
SignupEmailPolicyReasonInvalidEmail = "invalid_email"
SignupEmailPolicyReasonDisposableDomain = "disposable_domain"
SignupEmailPolicyReasonFreeDomain = "free_domain"
)
var ErrSignupEmailPolicyRejected = Define(SignupEmailPolicyCode)
type SignupEmailPolicyError struct {
reason string
domain string
}
func NormalizeSignupEmailPolicyReason(reason string) string {
switch strings.TrimSpace(reason) {
case SignupEmailPolicyReasonInvalidEmail:
return SignupEmailPolicyReasonInvalidEmail
case SignupEmailPolicyReasonDisposableDomain:
return SignupEmailPolicyReasonDisposableDomain
case SignupEmailPolicyReasonFreeDomain:
return SignupEmailPolicyReasonFreeDomain
default:
return SignupEmailPolicyReasonInvalidEmail
}
}
func IsSignupEmailPolicyReason(reason string) bool {
return strings.TrimSpace(reason) == NormalizeSignupEmailPolicyReason(reason)
}
func (e *SignupEmailPolicyError) Error() string {
if e == nil || e.reason == "" {
return SignupEmailPolicyCode
}
return SignupEmailPolicyCode + ": " + e.reason
}
func (e *SignupEmailPolicyError) Unwrap() error {
return ErrSignupEmailPolicyRejected
}
func (e *SignupEmailPolicyError) Reason() string {
if e == nil {
return ""
}
return strings.TrimSpace(e.reason)
}
func (e *SignupEmailPolicyError) Domain() string {
if e == nil {
return ""
}
return strings.TrimSpace(e.domain)
}
func SignupEmailPolicyRejected(reason, domain string) error {
return &SignupEmailPolicyError{
reason: NormalizeSignupEmailPolicyReason(reason),
domain: strings.TrimSpace(domain),
}
}
func AsSignupEmailPolicyError(err error) (*SignupEmailPolicyError, bool) {
var policyErr *SignupEmailPolicyError
if errors.As(err, &policyErr) && policyErr != nil {
return policyErr, true
}
return nil, false
}
@@ -0,0 +1,52 @@
package merrors
import (
"errors"
"testing"
)
func TestSignupEmailPolicyError(t *testing.T) {
err := SignupEmailPolicyRejected(SignupEmailPolicyReasonFreeDomain, "gmail.com")
policyErr, ok := AsSignupEmailPolicyError(err)
if !ok {
t.Fatalf("expected signup email policy error")
}
if policyErr.Reason() != SignupEmailPolicyReasonFreeDomain {
t.Fatalf("reason mismatch: got=%q", policyErr.Reason())
}
if policyErr.Domain() != "gmail.com" {
t.Fatalf("domain mismatch: got=%q", policyErr.Domain())
}
if !errors.Is(err, ErrSignupEmailPolicyRejected) {
t.Fatalf("expected signup email policy sentinel")
}
reason, ok := Reason(err)
if !ok {
t.Fatalf("expected generic reason")
}
if reason != SignupEmailPolicyReasonFreeDomain {
t.Fatalf("generic reason mismatch: got=%q", reason)
}
}
func TestSignupEmailPolicyReasonNormalization(t *testing.T) {
for _, reason := range []string{
SignupEmailPolicyReasonInvalidEmail,
SignupEmailPolicyReasonDisposableDomain,
SignupEmailPolicyReasonFreeDomain,
} {
if !IsSignupEmailPolicyReason(reason) {
t.Fatalf("expected supported reason: %q", reason)
}
}
err := SignupEmailPolicyRejected("unexpected_reason", "")
policyErr, ok := AsSignupEmailPolicyError(err)
if !ok {
t.Fatalf("expected signup email policy error")
}
if policyErr.Reason() != SignupEmailPolicyReasonInvalidEmail {
t.Fatalf("unexpected reasons must not leak: got=%q", policyErr.Reason())
}
}
+50
View File
@@ -0,0 +1,50 @@
package merrors
import (
"errors"
"strings"
"time"
)
// ThrottleDetails describes a temporary request limit without coupling callers
// to a transport-specific response format.
type ThrottleDetails struct {
ScopeKind string
RetryAfter time.Duration
}
type throttleDetailsError struct {
base error
msg string
details ThrottleDetails
}
func (e throttleDetailsError) Error() string {
return Wrap(e.base, e.msg).Error()
}
func (e throttleDetailsError) Unwrap() error {
return e.base
}
// WithThrottle annotates an error with retry information while preserving its
// classification for errors.Is and errors.As.
func WithThrottle(base error, scopeKind string, retryAfter time.Duration, msg string) error {
return throttleDetailsError{
base: base,
msg: msg,
details: ThrottleDetails{
ScopeKind: strings.TrimSpace(scopeKind),
RetryAfter: retryAfter,
},
}
}
// ThrottleDetailsFrom returns retry information attached by WithThrottle.
func ThrottleDetailsFrom(err error) (ThrottleDetails, bool) {
var throttled throttleDetailsError
if !errors.As(err, &throttled) {
return ThrottleDetails{}, false
}
return throttled.details, true
}
+35
View File
@@ -0,0 +1,35 @@
package merrors
import (
"errors"
"testing"
"time"
)
func TestWithThrottlePreservesClassificationAndDetails(t *testing.T) {
base := Define("verificationLimited")
err := Wrap(
WithThrottle(base, " destination ", 45*time.Second, "request is temporarily limited"),
"verification failed",
)
if !errors.Is(err, base) {
t.Fatalf("throttle error did not preserve classification: %v", err)
}
details, ok := ThrottleDetailsFrom(err)
if !ok {
t.Fatal("throttle details were not available through wrapping")
}
if details.ScopeKind != "destination" || details.RetryAfter != 45*time.Second {
t.Fatalf("unexpected throttle details: %+v", details)
}
if got, want := err.Error(), "verificationLimited: request is temporarily limited: verification failed"; got != want {
t.Fatalf("unexpected throttle error text: got=%q want=%q", got, want)
}
}
func TestThrottleDetailsFromRejectsUnannotatedError(t *testing.T) {
if details, ok := ThrottleDetailsFrom(ServiceUnavailable("dependency unavailable")); ok {
t.Fatalf("unexpected throttle details: %+v", details)
}
}
+15
View File
@@ -0,0 +1,15 @@
package merrors
var ErrTreasuryInvalidArgument = Define("treasury.monitoring.invalid_argument")
var ErrTreasuryConfiguration = Define("treasury.monitoring.configuration_error")
var ErrTreasuryNotFound = Define("treasury.monitoring.not_found")
var ErrTreasuryOutOfOrder = Define("treasury.monitoring.out_of_order")
var ErrTreasuryNegativeBalance = Define("treasury.monitoring.negative_balance")
var ErrTreasuryAlreadyReversed = Define("treasury.monitoring.movement_already_reversed")
var ErrTreasuryProjectionMismatch = Define("treasury.monitoring.projection_mismatch")