Documentation
¶
Overview ¶
Package lunixi is a server-side client for Lunixi's signed merchant APIs.
The client signs requests with Ed25519, caches short-lived access tokens, preserves integer minor-unit money, and verifies raw-body v2 webhooks. Secret keys belong on the merchant server; browser checkout uses the short-lived token returned by Payments.CreateIntent.
Index ¶
- Constants
- Variables
- func CanonicalJSON(value any) ([]byte, error)
- func CanonicalRequest(method, path, date, nonce, digest string) string
- func Digest(body []byte) string
- type APIError
- type Address
- type BasketItem
- type Buyer
- type Client
- type Config
- type FraudClient
- func (f *FraudClient) Decision(ctx context.Context, traceID string) (*Response, error)
- func (f *FraudClient) Decisions(ctx context.Context, filters url.Values) (*Response, error)
- func (f *FraudClient) Evaluate(ctx context.Context, body map[string]any) (*Response, error)
- func (f *FraudClient) RecordEvent(ctx context.Context, event map[string]any, key string) (*Response, error)
- type IntentRequest
- type PaymentClient
- func (p *PaymentClient) Capture(ctx context.Context, id string, amount *int64, key string) (*Response, error)
- func (p *PaymentClient) CreateIntent(ctx context.Context, r IntentRequest, key string) (*Response, error)
- func (p *PaymentClient) Get(ctx context.Context, id string) (*Response, error)
- func (p *PaymentClient) GetSplit(ctx context.Context, id string) (*Response, error)
- func (p *PaymentClient) List(ctx context.Context, filters url.Values) (*Response, error)
- func (p *PaymentClient) Methods(ctx context.Context) (*Response, error)
- func (p *PaymentClient) Refund(ctx context.Context, id string, amount *int64, reason, key string) (*Response, error)
- func (p *PaymentClient) RefundSplit(ctx context.Context, id string, amount int64, reason, key string) (*Response, error)
- func (p *PaymentClient) Split(ctx context.Context, body map[string]any, key string) (*Response, error)
- func (p *PaymentClient) Void(ctx context.Context, id, key string) (*Response, error)
- type ReplayStore
- type Request
- type Response
- type SQLInbox
- type WebhookConfig
- type WebhookEvent
- type WebhookVerifier
Constants ¶
const MaxSafeInteger int64 = 9007199254740991
MaxSafeInteger is the largest integer JSON.stringify preserves exactly.
const Version = "0.1.1"
Version is the SDK semantic version.
Variables ¶
var ( // ErrConfiguration indicates invalid configuration or a rejected request. ErrConfiguration = errors.New("lunixi: invalid configuration or request") // ErrWebhook indicates an invalid, stale, or mismatched webhook. ErrWebhook = errors.New("lunixi: webhook verification failed") )
var ErrInbox = errors.New("lunixi: webhook inbox persistence failed")
ErrInbox indicates a durable database failure. Return a retryable HTTP status, not a successful webhook acknowledgement, when it occurs.
Functions ¶
func CanonicalJSON ¶
CanonicalJSON produces compact JSON with JavaScript's numeric-index key ordering, UTF-8 strings, and number formatting. Unsafe integer numbers and duplicate object keys are rejected; use strings where an API specifies them.
func CanonicalRequest ¶
CanonicalRequest returns the exact string signed by the gateway protocol.
Types ¶
type APIError ¶
APIError exposes safe diagnostics without including a request body, access token, private key, customer information, or an upstream error message.
type Address ¶
type Address struct {
ContactName string `json:"contactName,omitempty"`
Address string `json:"address,omitempty"`
City string `json:"city,omitempty"`
Country string `json:"country,omitempty"`
ZipCode string `json:"zipCode,omitempty"`
}
Address is a billing or shipping address accepted by the payment API.
type BasketItem ¶
type BasketItem struct {
ID string `json:"id"`
Name string `json:"name"`
ItemType string `json:"itemType"`
Price int64 `json:"price"`
Category1 string `json:"category1,omitempty"`
Category2 string `json:"category2,omitempty"`
InsuranceData map[string]any `json:"insuranceData,omitempty"`
InsuranceCategory string `json:"insuranceCategory,omitempty"`
InsuranceTypes []string `json:"insuranceTypes,omitempty"`
}
BasketItem is a checkout basket line; Price is an integer minor-unit line total, not a per-unit float. Additional insurance data follows the gateway schema and is passed without guessing insurance-specific field rules.
type Buyer ¶
type Buyer struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Surname string `json:"surname,omitempty"`
IdentityNumber string `json:"identityNumber,omitempty"`
Email string `json:"email,omitempty"`
GSMNumber string `json:"gsmNumber,omitempty"`
IP string `json:"ip,omitempty"`
City string `json:"city,omitempty"`
Country string `json:"country,omitempty"`
ZipCode string `json:"zipCode,omitempty"`
RegistrationDate string `json:"registrationDate,omitempty"`
LastLoginDate string `json:"lastLoginDate,omitempty"`
RegistrationAddress string `json:"registrationAddress,omitempty"`
}
Buyer holds the documented buyer details used by hosted checkout. Values are sent to Lunixi only and must never be copied to application logs.
type Client ¶
type Client struct {
Payments *PaymentClient
Fraud *FraudClient
// contains filtered or unexported fields
}
Client is safe for concurrent use. Create one client per merchant key.
func (*Client) Do ¶
Do calls a documented merchant endpoint through the same signed transport as the high-level clients. It does not invent service-specific validation; callers using this escape hatch must follow the endpoint's public schema.
func (*Client) InvalidateToken ¶
func (c *Client) InvalidateToken()
InvalidateToken clears the in-memory token cache, for example after key permissions change. Key rotation itself requires constructing a new client.
type Config ¶
type Config struct {
BaseURL string
KeyID string `json:"-"`
PrivateKey string `json:"-"`
HTTPClient *http.Client
Timeout time.Duration
MaxRetries int
MaxResponseBytes int64
// AllowInsecureLocalhost permits HTTP only for literal loopback addresses
// and localhost. It is intended for local test servers, never production.
AllowInsecureLocalhost bool
// Now provides a deterministic clock for tests. Nil uses time.Now.
Now func() time.Time
}
Config configures one merchant's client. Do not reuse a client across keys or merchants. HTTPClient is copied and redirects are always disabled.
func (Config) MarshalJSON ¶
MarshalJSON intentionally excludes configuration secrets from structured logs.
type FraudClient ¶
type FraudClient struct {
// contains filtered or unexported fields
}
FraudClient exposes transaction evaluation and event ingestion. Results remain gateway decisions; callers must enforce their own business policy.
func (*FraudClient) Decisions ¶
Decisions lists fraud decisions without discarding sibling pageInfo.
func (*FraudClient) Evaluate ¶
Evaluate rejects unknown top-level fields, including tenantId, before authentication. Tenant identity always comes from the merchant credential.
func (*FraudClient) RecordEvent ¶
func (f *FraudClient) RecordEvent(ctx context.Context, event map[string]any, key string) (*Response, error)
RecordEvent ingests one transaction event. A stable eventId and caller key are required by this helper for durable application-level idempotency.
type IntentRequest ¶
type IntentRequest struct {
Amount int64 `json:"amount"`
Currency string `json:"currency"`
OrderID string `json:"orderId"`
CallbackURL string `json:"callbackUrl"`
Buyer *Buyer `json:"buyer,omitempty"`
BillingAddress *Address `json:"billingAddress,omitempty"`
ShippingAddress *Address `json:"shippingAddress,omitempty"`
BasketItems []BasketItem `json:"basketItems,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
Preauth bool `json:"preauth,omitempty"`
Locale string `json:"locale,omitempty"`
Description string `json:"description,omitempty"`
CustomerID string `json:"customerId,omitempty"`
CardUserKey string `json:"cardUserKey,omitempty"`
PaymentMethod string `json:"paymentMethod,omitempty"`
WalletProvider string `json:"walletProvider,omitempty"`
PaymentChannel string `json:"paymentChannel,omitempty"`
PaymentGroup string `json:"paymentGroup,omitempty"`
Force3D *bool `json:"force3D,omitempty"`
Settings map[string]any `json:"settings,omitempty"`
}
IntentRequest creates a checkout form token. Amount is the basket total before a provider-calculated installment fee. Installments are selected in the shared form, not prescribed in the intent request.
type PaymentClient ¶
type PaymentClient struct {
// contains filtered or unexported fields
}
PaymentClient implements checkout forms, payment lifecycle and split payment merchant endpoints. It never determines an order's paid state.
func (*PaymentClient) Capture ¶
func (p *PaymentClient) Capture(ctx context.Context, id string, amount *int64, key string) (*Response, error)
Capture captures the full authorization when amount is nil, or a specified positive integer amount. It must reference the original payment account.
func (*PaymentClient) CreateIntent ¶
func (p *PaymentClient) CreateIntent(ctx context.Context, r IntentRequest, key string) (*Response, error)
CreateIntent creates a hosted checkout session. Persist key per payment attempt before calling; reuse it after a timeout instead of charging again.
func (*PaymentClient) GetSplit ¶
GetSplit returns all legs and aggregate amounts; COMPLETED alone should not replace amount/currency/balancing/reconciliation checks in the order adapter.
func (*PaymentClient) List ¶
List returns the original gateway envelope including pagination metadata.
func (*PaymentClient) Methods ¶
func (p *PaymentClient) Methods(ctx context.Context) (*Response, error)
Methods reads the merchant's enabled method/provider capabilities.
func (*PaymentClient) Refund ¶
func (p *PaymentClient) Refund(ctx context.Context, id string, amount *int64, reason, key string) (*Response, error)
Refund refunds the full refundable payment when amount is nil, or a partial amount. Unknown outcomes must be reconciled with the same key.
func (*PaymentClient) RefundSplit ¶
func (p *PaymentClient) RefundSplit(ctx context.Context, id string, amount int64, reason, key string) (*Response, error)
RefundSplit asks the gateway to distribute a refund across refundable legs. Partial or failed allocations return an APIError and require review.
func (*PaymentClient) Split ¶
func (p *PaymentClient) Split(ctx context.Context, body map[string]any, key string) (*Response, error)
Split validates the documented split amount/leg contract and starts or continues it. Supply the same body and key for every continuation. Pending split states can be returned on HTTP 400 and remain explicitly unpaid.
type ReplayStore ¶
type ReplayStore interface {
Process(context.Context, *WebhookEvent, func(*sql.Tx) error) (bool, error)
}
ReplayStore applies a verified event and its event-ID marker in the SAME transaction. The callback must use the supplied transaction for business state. Network requests and other nontransactional effects belong in a durable application outbox, not inside this callback.
type Request ¶
Request is a signed merchant API request. Path must be an unescaped path under /api/v1/; use Query for query parameters. A stable IdempotencyKey is required by financial mutation helpers and controls safe automatic retries.
type Response ¶
type Response struct {
StatusCode int
RequestID string
Envelope json.RawMessage
Data json.RawMessage
PageInfo json.RawMessage
ContentType string
Bytes []byte
}
Response preserves the gateway envelope, including pageInfo and pending financial states. Inspect verified amount, currency and status before order fulfillment. A successful HTTP response alone is not proof of payment.
func (*Response) DecodeData ¶
DecodeData decodes data without converting integer amounts to float64. A map/interface target receives json.Number values.
type SQLInbox ¶
type SQLInbox struct {
// contains filtered or unexported fields
}
SQLInbox is a durable ReplayStore for PostgreSQL and SQLite. Database drivers are supplied by the application; the SDK has no runtime dependency. Its table stores IDs, timestamp and a payload hash, never webhook raw data.
func NewSQLInbox ¶
NewSQLInbox validates a separate deduplication scope for each merchant or integration. table must be a trusted migration-time SQL identifier.
func (*SQLInbox) EnsureSchema ¶
EnsureSchema explicitly creates the inbox table. Production applications can apply the equivalent DDL in their normal migration process instead.
func (*SQLInbox) Process ¶
func (s *SQLInbox) Process(ctx context.Context, event *WebhookEvent, apply func(*sql.Tx) error) (bool, error)
Process returns true only when the callback and event marker are committed. A duplicate returns false without running callback. An event-ID collision with different signed content fails closed. Callback failure rolls back its marker so the provider can safely retry after an application crash/error.
type WebhookConfig ¶
WebhookConfig verifies current and previous rotation secrets. Tolerance defaults to five minutes and cannot be disabled in production verification.
type WebhookEvent ¶
type WebhookEvent struct {
// contains filtered or unexported fields
}
WebhookEvent is an authenticated immutable event. Accessors return copies; unverified events cannot be constructed through exported fields.
func (*WebhookEvent) Data ¶
func (e *WebhookEvent) Data() json.RawMessage
Data returns an independent copy of the signed data object.
func (*WebhookEvent) DecodeData ¶
func (e *WebhookEvent) DecodeData(target any) error
DecodeData decodes the signed data using json.Number for interface values.
func (*WebhookEvent) ID ¶
func (e *WebhookEvent) ID() string
ID is the event ID bound to the raw-body signature.
func (*WebhookEvent) Raw ¶
func (e *WebhookEvent) Raw() []byte
Raw returns an independent copy of the signed body. It can contain customer data: do not log it or persist it outside your data-retention policy.
func (*WebhookEvent) Timestamp ¶
func (e *WebhookEvent) Timestamp() time.Time
Timestamp is the verified signature timestamp.
func (*WebhookEvent) Type ¶
func (e *WebhookEvent) Type() string
Type is read from the signed JSON body, never an unsigned header fallback.
type WebhookVerifier ¶
type WebhookVerifier struct {
// contains filtered or unexported fields
}
WebhookVerifier authenticates raw request bytes. Verification does not consume an event ID: use a durable transactional ReplayStore before applying financial state, so a crash cannot lose a previously claimed event.
func NewWebhookVerifier ¶
func NewWebhookVerifier(config WebhookConfig) (*WebhookVerifier, error)
NewWebhookVerifier validates webhook configuration without handling data.
func (*WebhookVerifier) Verify ¶
func (v *WebhookVerifier) Verify(raw []byte, headers http.Header) (*WebhookEvent, error)
Verify checks the exact raw bytes, event ID, ISO timestamp, body/header ID agreement, type and rotated v2 signatures. Unknown schemes do not authorize a request. Read the body with an application-level size limit before calling.
func (*WebhookVerifier) VerifyRequest ¶
func (v *WebhookVerifier) VerifyRequest(ctx context.Context, request *http.Request) (*WebhookEvent, error)
VerifyRequest reads an HTTP request with a fixed limit before verification. The caller remains responsible for applying ReplayStore transactionally.