package sendico import ( "context" "net/http" "git.sendico.io/tech/sxchnge/product/payments" merrors "git.sendico.io/tech/sxchnge/shared/errors" ) func (c *Client) validCustomerAccount(a accountDTO, subject string) bool { return validRef(a.ID) && a.ScopeRef == c.config.OrganizationID && a.Owner == "" && a.Type == "ACCOUNT_TYPE_ASSET" && a.LedgerRole == "ACCOUNT_ROLE_OPERATING" && a.Status == "ACCOUNT_STATUS_ACTIVE" && !a.AllowNegative && a.Metadata["sxchnge_subject_ref"] == subject && (a.Metadata["external_subject_ref"] == "" || a.Metadata["external_subject_ref"] == subject) } // EnsureAccounts also repairs older customers, using provider-enforced // organization/subject/currency uniqueness for safe retries after lost responses. func (c *Client) EnsureAccounts(ctx context.Context, subject string) error { if !validRef(subject) { return payments.Unavailable } root := "/ledger_accounts/" + c.config.OrganizationID var data accountListDTO if err := c.paymentRequest(ctx, http.MethodGet, root, nil, &data); err != nil { return err } existing := map[string]bool{} reconcile := map[string]bool{} for _, a := range data.Accounts { if a.Metadata["sxchnge_subject_ref"] != subject && a.Metadata["external_subject_ref"] != subject { continue } if !c.validCustomerAccount(a, subject) || existing[a.Currency] { return payments.Unavailable } existing[a.Currency] = true reconcile[a.Currency] = a.Metadata["external_subject_ref"] == subject } missing := []string{} for _, code := range []string{"RUB", "USDT", "USDC"} { if !existing[code] || reconcile[code] { missing = append(missing, code) } } if len(missing) == 0 { return nil } // Older providers can accept the metadata but cannot prevent duplicates. // Require the explicit capability before issuing the first create. if !data.ExternalSubjectAccounts { return payments.Unavailable } var catalog struct { Rows []currencyDTO `json:"currencies"` } if err := c.paymentRequest(ctx, http.MethodGet, "/payments/fx/currencies/"+c.config.OrganizationID, nil, &catalog); err != nil { return err } for _, code := range missing { found := false for _, currency := range catalog.Rows { if currency.Code == code && currency.Scale != nil && *currency.Scale >= 0 && *currency.Scale <= 8 { found = true } } if !found { return merrors.OpaqueWrap(payments.Unavailable, payments.Unsupported, "default currency missing from provider catalog") } } // Replaying a bound create also resumes Sendico's durable wallet/route setup // if a previous response failed after the ledger itself had been committed. for _, code := range missing { var response struct { Account accountDTO `json:"account"` } request := createAccountDTO{Currency: code, Type: "asset", LedgerRole: "operating", Metadata: map[string]string{"external_subject_ref": subject, "sxchnge_subject_ref": subject}} if err := c.paymentRequest(ctx, http.MethodPost, root, request, &response, http.StatusCreated); err != nil { return err } if !c.validCustomerAccount(response.Account, subject) || response.Account.Currency != code || response.Account.Metadata["external_subject_ref"] != subject { return payments.Unavailable } } return nil }