Download the PHP package progalaxyelabs/stonescriptphp-payments without Composer

On this page you can find all versions of the php package progalaxyelabs/stonescriptphp-payments. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package stonescriptphp-payments

stonescriptphp-payments

The unified payment system for StoneScriptPHP. This single package merges what used to be three separately-versioned pieces — stonescriptphp-pay (gateway transport drivers), stonescriptphp-invoice (SQL-first billing/invoicing), and the glue between them — into one repo, one composer package, one version, one schema load-order, one test suite. There is no longer a "which package owns this" question for payment capture, invoicing, or tax: it's all here.

Three cooperating layers:

  1. Drivers (src/Drivers/) — thin PHP transport for payment gateways (Razorpay, PayPal today). Create orders, verify checkout responses, parse webhooks. No business logic — every driver implements the shared PaymentProvider contract.
  2. Capture (src/Capture/Schema/) — the canonical, idempotent payment-capture SQL primitive (pay_captured_payments + pay_record_captured_payment()). Records "this gateway transaction was captured, exactly once" and nothing else — no wallet credit, no subscription extension, no invoice transition. A consumer's own domain function composes its side effect on top of it (see that function's docblock for the exact composition contract).
  3. Invoicing/Pricing/Metering/Tax/Outbox (src/Invoicing/, src/Pricing/, src/Metering/, src/Tax/, src/Outbox/, src/Php/InvoiceSourceAdapter.php) — SQL-first billing: plans, subscriptions, wallets, credit ledgers, passthrough cost-plus, postpaid charges, persisted invoices, configurable invoice numbering, parties, a pluggable regional tax layer (ships a ready India-GST-compatible implementation under Tax/India), data-driven gateway routing, and the durable transactional-outbox intimation to a central merchant-of-record. Business logic lives in PostgreSQL functions deployed via the StoneScriptDB Gateway — PHP is transport only.

Architecture: logic in Postgres, PHP is transport only

Ecosystem law for StoneScriptPHP is that business logic lives in SQL functions (one tested place, one fix lands once) and PHP is a thin transport/packaging layer. Money is integer minor units (paisa/cents) end to end; quantity is integer milli-units; fx-rate is a decimal string — never a float.

Multi-country tax (scaffolded, not yet wired): inv_tax_scheme + inv_resolve_tax_scheme() (033, src/Tax/Schema/) resolve which tax regime governs a billing country by a plain JOIN — the SAME pattern inv_resolve_gateway already uses for payment-gateway routing (data + JOIN + a CASE on a data column), never a dynamically-dispatched/EXECUTEd function name. Today exactly one scheme is seeded ('IN' -> 'in_gst'), and inv_compute_invoice (030, Tax/India) IS that scheme's engine, called directly and statically as before — this table is additive scaffolding so the next country is a data INSERT (+ at most one new hardcoded CASE branch in a future country-agnostic dispatcher), never a new "pass a function name" parameter anywhere.

Features: Pricing (plans, customers, subscription core), Metering (wallet, credit ledger, passthrough cost-plus, postpaid, subscription+overage), Tax/India (the included India-GST regional tax pack — money primitives, state/region codes, config/slabs, invoice-number formatter, party, line/invoice validation, tax engine, dated-FX reference value), Invoicing (persisted invoices, payments, gateway routing, status lifecycle, CRM chase projection), Outbox (transactional-outbox product<->central intimation, both directions), Capture (the gateway-agnostic idempotent capture primitive). Swap Tax/India for your own region's tax pack to bill under different rules.

All SQL schema is the main scope; a per-tenant scope layer is deferred.

Framework compatibility

The SQL, the drivers and the pure-PHP classes (EntitlementGuard, EntitlementOperations, WalletOperations, the webhook ingress and inbox) need no framework. The framework adapters require StoneScriptPHP >= 12.0 and are tested against 12.0.0: Entitlement\EntitlementMiddleware and FrameworkMainDbScope (tenant-context handling over the gateway client), Webhook\DatabaseFnCaller (Database::fn, and Database::mutate for the functions you name: the 12.0 write boundary, off by default), Routes\PostWebhookRoute, and Php\InvoiceSourceAdapter. The library passes only scalar arguments to Database::fn, so the 12.0 raw-array deprecation never fires; its routes answer with their own ApiResponse (no exception text reaches a client), register POST only (the 12.0 HEAD and 405 rules do not affect them), and every tenant id it handles is a UUID (the 12.0 tenant-id rule). Do not pin a platform to an older framework to use these adapters.

Installation

Driver usage

The PaymentProvider contract

Every driver implements settlementModel(): string, returning 'gateway' (the consuming application is merchant of record and owes GST/tax invoicing — Razorpay, PayPal) or 'mor' (the provider is seller of record and handles tax itself — e.g. a future Paddle driver). Framework glue such as StoneScriptPHP\Billing\CollectionOrchestrator uses this to decide whether an invoicing side (InvoiceSource) is expected.

PayPal driver

PaypalDriver ports a bespoke PayPal Orders v2 client (raw curl, no SDK) into the shared contract — one-time order/capture only, PayPal Subscriptions is NOT implemented (createSubscription() / cancelSubscription() / getSubscription() throw PaymentException).

verifySignature() is NOT side-effect-free for this driver — calling it actually CAPTURES the order (moves funds). PayPal's Orders v2 flow has no signature distinct from capture, so the capture call itself IS the authoritative confirmation. It is idempotent against retries (a deterministic PayPal-Request-Id header, plus explicit handling of a 422 ORDER_ALREADY_CAPTURED response as success rather than a signature failure) — but know that this method performs a real, funds-moving API call before you call it from anywhere that might retry blindly.

Known gap: minor-unit <-> decimal-string conversion assumes a 2-decimal currency (USD/EUR/INR/...) — a 0-decimal currency (e.g. JPY) is not supported as written.

Capture primitive

pay_record_captured_payment() is THE canonical, safe-by-default idempotent capture primitive for the whole StoneScriptPHP ecosystem. Call it as the first statement of your own domain function (e.g. an activation or credit function), inside the same transaction, before performing your own side effect:

Idempotency key is (gateway_code, gateway_txn_ref) — a plain UNIQUE index plus INSERT ... ON CONFLICT DO NOTHING is the entire concurrency guarantee (no advisory lock, no row lock — see the function's docblock for why that's sufficient). Deploy src/Capture/Schema/ alongside the rest of this package's schema.

Invoicing side: InvoiceSourceAdapter

src/Php/InvoiceSourceAdapter.php implements StoneScriptPHP\Billing\Contracts\InvoiceSource (published by progalaxyelabs/stonescriptphp v9.17.0+'s Billing\ seam) by calling inv_invoice_payability / inv_record_payment via Database::fn(). It is pure marshalling — it computes no amount, tax, or routing decision; every value it returns is one the SQL function already decided. Wire it into StoneScriptPHP\Billing\CollectionOrchestrator to drive automated collection:

This is optional — progalaxyelabs/stonescriptphp is only suggest-ed (+ require-dev for this package's own tests), not required. Pure-SQL usage (issue invoices, run the CRM chase, mark paid manually) and the payment-driver side need nothing but PHP itself.

Deploy

Load every src/**/*.pgsql file in the global numeric-prefix order (see LOAD-ORDER.md) — in a StoneScriptPHP project this is the StoneScriptDB Gateway migration path (php stone gateway:migrate). The schema loads standalone into an empty database with no test/seed dependency:

Issuers, numbering series and lawful invoice numbers

Statutory serial numbers (CGST Rules 2017, Rule 46(b): consecutive, at most 16 characters, letters / numerals / hyphen / slash, unique per financial year, in one or several series) are allocated by the library, never by the caller:

Review hardening (all enforced in SQL, all tested):

SQLSTATEs: PY010 not found, PY011 inactive / wrong kind, PY012 mode, PY013 placeholder issuer, PY014 invalid definition, PY015 seller conflicts with issuer, PY016 number/series combination, PY017 exhausted or counter behind, PY018 counter backwards, PY019 immutability, PY040 schema upgrade missing, PY041 direct allocation, PY042 document date outside the window.

Load-bearing: run _pay_apply_schema_upgrades() before ANY order or capture traffic on a database that already held invoices. The numbering functions, row guards and the capture path need the columns it adds; until it has run they refuse with PY040 naming the missing migration (a write in that window fails loudly and is retried; nothing is corrupted). Deploy order: gateway migrate (which runs the platform's migration containing the call) first, application traffic second.

Upgrading a database that already holds invoices: _pay_apply_schema_upgrades()

The gateway creates a table from its file only when the table is missing; it never alters an existing one (new columns, a relaxed NOT NULL or a new index in a table file are reported as safe but not applied). New tables need nothing. For tables that already exist the library ships an additive, idempotent function, run from the platform's own migration together with the reference-data one:

It returns one row per change it made (none when current). Run the migrate at a quiet moment: the gateway deploys functions (including the new row guards) before it runs migrations, so for the length of that one request a write to inv_invoices can fail loudly until the columns exist.

Webhooks: a durable inbox, acknowledged only after it is stored

StoneScriptPay\Http\Webhook\WebhookIngress is the endpoint logic (framework-independent); StoneScriptPay\Http\Routes\PostWebhookRoute is the StoneScriptPHP route over it (register it as a public route: the gateway's signature, verified by the driver, is the authentication).

  1. The driver verifies the signature before parsing. A bad one is 401 and writes nothing.
  2. The event is stored (pay_webhook_record) - the durability point. If that fails the answer is 503, the gateway redelivers, and the event is applied exactly once when it finally arrives.
  3. Only then is it processed and acknowledged. A processing failure leaves a failed row (kept, replayable, alerted once) and still answers 200: the event is durable, a retry would only redeliver a duplicate.
outcome meaning
applied processed; the processor must be idempotent (it can be re-run after a crash)
ignored a verified event type with no handler
foreign addressed to another platform on a shared gateway account (read from the order notes): recorded, never applied, never alerted
unattributed a payment event with no platform routing key: recorded, never applied by guess, alerts once
failed processing raised, or a signed body was unreadable (stored raw, byte for byte)

The classification is exact: an event carrying our platform code but no order of ours, or one of our order references that does not resolve, is unattributed; an event with no routing key and no order of ours at all (someone else's payment on a shared gateway account) is foreign - recorded, never replayed, never alerted.

Dedupe key = SHA-256 of the raw body, the bytes the gateway's signature covers. The gateway's own event id travels in an unsigned header (Razorpay), so a forged id could pre-empt another event's genuine delivery or let an old signed body dodge dedupe; a genuine redelivery is byte-identical, so it still dedupes. The header id is kept as metadata.

Replay cannot starve. An open row (failed / unattributed) is retried with exponential backoff (webhook_backoff_base_seconds doubling, capped), fewest attempts first, and after webhook_max_attempts attempts or webhook_dead_after_days days it becomes dead: terminal, never claimed or replayed, alerted once on that transition (_pay_webhook_requeue puts it back by hand). Every alert is stamped as sent (pay_webhook_alert_sent) only after the sink succeeded, so a failing sink leaves it due instead of consuming it. All of these are pay_config rows.

Rate limiting. The route is public until the signature is verified, and PayPal verification costs an outbound call. WebhookIngress takes an optional RateLimiter and answers 429 before verifying anything; the library ships none (it cannot assume a shared store) - back it with the counter the platform already uses, or an nginx limit_req zone in front. The PayPal OAuth token is cached until a minute before it expires (and refreshed once on a 401).

What is stored where (privacy)

Data Where Personal data? Kept
Normalised webhook event (event type, gateway payment id, order ref, amount, currency, tenant, platform, time) pay_webhook_inbox.event no with the inbox row
Raw webhook body pay_webhook_inbox.raw_body may (payer e-mail, phone, card/UPI details as the gateway sent them) raw_payload_retention_months (default 13), then redacted to a record holding only its SHA-256 and the normalised event; each redaction audited in pay_webhook_redactions
Provider-native payload / duplicated raw body inside the stored event not stored - -
Request headers pay_webhook_inbox.headers no (allow-list; anything credential- or signature-like is dropped) with the row
Buyer name / GSTIN / state on an invoice inv_invoices.buyer_* yes (statutory) accounting_record_retention_years (8 financial years, Companies Act s.128); never touched by the payload job
Payments, orders, events inv_payments, pay_captured_payments, pay_orders, pay_events allow-listed non-personal fields + body hash only accounting records

Legal bases (CA / legal to confirm both periods): the DPDP Rules 2025 impose a minimum one-year retention for personal data, traffic data and processing logs, so raw payloads are kept 13 months (floor + buffer; also covers card-dispute windows) and then redacted by bin/pay-purge-webhooks (daily cron/systemd); accounting records are statutory for 8 financial years. The retention period of raw payloads is an owner decision; 13 months is the default. Both are pay_config rows.

A redelivered event (same signed bytes) is a no-op. Rows are retained records: never deleted, evidence columns immutable. Two deliveries (or a delivery and a replay) cannot process one event at once: a short lease is claimed first.

Replay re-drives failed, unattributed and stale received rows with WebhookReplayer, or from cron / systemd with vendor/bin/pay-replay-webhooks --bootstrap=replayer.php (the bootstrap file returns your configured WebhookReplayer; exit status 2 while any row is still open). Existing deployments that run a bespoke quarantine table can copy its rows into the inbox as failed / unattributed rows with pay_webhook_record(..., 'failed', <error>) so nothing already captured is lost.

Orders and the single exactly-once capture path

Every payment starts as a server-created order and ends in one function, pay_apply_capture. There is no second copy of "capture exactly once" to keep in step: the webhook processor, the operator path and any reconciliation job all call it.

Which functions are entry points (what a platform may call, and what it may not)

A SQL function reachable through the gateway's /call is an API. This package therefore marks its own setup and migration acts as internal with a leading underscore, so a gateway that refuses underscore names (StoneScriptDB Gateway 4.8+) cannot be asked to run them from the network, and the public surface is exactly the functions below.

_pay_webhook_redact_expired can only redact rows that are already final and past the retention period, never deletes anything, skips any transaction with unresolved money (above), and the inbox guard refuses any redaction that lacks the pay.redacting flag and its audit row in pay_webhook_redactions. It is still kept off /call: retention is a scheduled-job concern, not a network API.

Entitlement: one store, comps, the postpaid cycle, the access middleware

ent_entitlements is the one answer to "what is this tenant entitled to, and until when" (the framework's legacy subscriptions table and the pricing history in customer_pricing_plans are not consulted for access; the library writes the history in the same transaction). Everything reads ent_get(tenant, platform, product): the access middleware, an offline-licence reader, a scan-credit reader.

Who may call ent_get. ent_get (and, below, the wallet_* functions) take a tenant id and trust it: they are for the platform's server code, called with the tenant the platform has just authenticated (the JWT's), never with an id from a request body or a URL. Exposing them through a route that accepts a tenant id from the caller would let one tenant read another's entitlement and balance.

Admin actions: internal functions, CLI, and your own authorised route

The functions that give access away or move money-adjacent state are internal (leading underscore) and never reachable over the gateway's /call: _ent_comp_activate, _ent_set_status, _ent_enroll_postpaid, _ent_set_billing_mode, _ent_set_missing_mode, _ent_import_legacy, _ent_cutover_check, and the scheduled _ent_billing_due / _ent_billing_run_one / _ent_tick. Two ways to use them:

  1. The server tool bin/pay-entitlement <command> (comp, status, enroll-postpaid, billing-mode, missing-mode, import-legacy, cutover-check, billing-run, tick) with the platform's direct database credentials. Every mutating command requires --actor, which is written to the audit trail and logged. The scheduler lines are pay-entitlement billing-run and pay-entitlement tick; the clock comes from the database (--as-of is for rehearsals only).
  2. The platform's own admin route. The library authenticates no one. A platform that wants an in-app "comp this account" button writes a route that authenticates and authorises the operator itself, then calls EntitlementOperations::comp(...) (or the matching method) over its direct database connection, passing the authenticated operator id as $actor. Never wire an internal function to a route through the gateway client.

The access middleware

StoneScriptPay\Http\Entitlement\EntitlementMiddleware (framework adapter) over EntitlementGuard (pure PHP): reads ent_get on the main database (FrameworkMainDbScope clears the tenant context for the call and restores it, like the framework's own middleware), lets reads through, refuses writes by a read-only tenant with HTTP 423 and the error code (READ_ONLY_TRIAL_EXPIRED | READ_ONLY_PLAN_ENDED | READ_ONLY_PAYMENT_OVERDUE | READ_ONLY_NO_SUBSCRIPTION), and sets X-Subscription-State. The write allow-list keeps the framework's (renewal, export, data erasure, account recovery) plus the library's own money routes, which all live under ONE namespaced prefix, /pay (RoutePaths: /pay/orders, /pay/payments, /pay/quote, /pay/webhook/{gateway}), so a read-only tenant can always pay. /pay is the only library entry on the default write allow-list: no generic /orders or /payments is allowed on the library's behalf, so a platform's own routes of that name are not accidentally exempt. A platform that mounts the library elsewhere passes its prefix to EntitlementGuard (and to RoutePaths) and the allow-list follows it.

Bounded fail-open. Infrastructure trouble must not lock customers out, but a blip must not make a lapsed tenant writable. On a TRANSPORT failure the middleware serves the last-known decision for that tenant (up to 10 minutes) and otherwise serves open; after N consecutive failures it alerts once (entitlement.lookup_failing). A DEPLOY failure (ent_get missing, PY050, an empty result) is a bug, not a blip: logged at ERROR as ENTITLEMENT_DEPLOY_ERROR, alerted (entitlement.deploy_error) and still served. On every fail-open path no X-Subscription-State header is sent.

Cutover runbook: replacing a legacy subscriptions table

The hard rule: *the view replaces the table only after every legacy writer has moved to `ent_`.** A legacy table that is still being written while a view stands in for it silently loses those writes.

  1. Set the platform's missing mode to match the legacy platform's (pay-entitlement missing-mode --platform P --mode ...).
  2. pay-entitlement import-legacy --legacy subscriptions (idempotent, audited; a non-UUID tenant or unknown status is counted invalid, never guessed).
  3. Move every legacy writer to ent_* / pay_apply_capture: the sub_* functions, signup, webhooks, trial start. Deploy it.
  4. In ONE window: stop the legacy writers, run pay-entitlement import-legacy --legacy subscriptions --sync (the delta import: renewals written to the legacy table after the first import are carried over; an entitlement that has since moved on in ent_* is left alone), then pay-entitlement cutover-check --legacy subscriptions --platform P --legacy-missing-mode .... The check raises (PY067) unless the verifier returns zero differences, no legacy row is invalid, and the missing modes agree.
  5. Only then rename the legacy table to an archive (never dropped by this package) and CREATE VIEW subscriptions AS SELECT * FROM ent_legacy_subscriptions.

A legacy row with no expires_at is treated as already expired at its cancellation time (a cancelled row with no end is ended, never active forever).

Wallet v2: idempotent, locked, FIFO, milli-credits, double-entry

Prepaid credit with a hard stop at zero. The v1 wallet_* functions (topup_wallet, grant_free_credits, record_wallet_drain) remain for platforms that have not migrated but are deprecated: no lock, no idempotency key, whole credits only. pay_apply_capture no longer uses them. _wlt_mark_platform_migrated(platform) (or pay-wallet mark-migrated) is the per-platform switch: from then on those v1 functions raise (PY074) for that platform, so nothing can mint or debit the old wallet behind the new one's back.

Reference data: _pay_ensure_reference_data()

The StoneScriptDB Gateway seeds reference rows only when it creates a database. On a database that already exists, migrate merely verifies them and fails with MISSING SEEDER RECORDS when any are absent, which is exactly the state of a database created before this package was staged (or one whose seeding stopped part-way). Add one migration to the consuming platform's own migrations:

It inserts every missing reference row (14 tables, foreign-key order) and nothing else: existing rows are never updated or deleted, so local edits survive, and a second run changes nothing. Gateways are seeded inactive: their checkout endpoint is a placeholder, so a deployment sets the real endpoint and activates the gateways it uses. The GST slab set includes 40% (GST 2.0) and keeps 12% / 28% only for historical documents (legacy_rate_slabs); the set is a tax judgement for the CA to confirm. It returns one row per table with the number of rows it inserted, so an operator can see what was missing. The gateway applies a migration file once, so a package release that adds reference rows needs one new migration of the same one-liner. The function is generated from the seeders by scripts/generate-reference-data.php (--check is part of the test suite), so the seeders stay the single source of truth.

now() defaults

Every timestamp that is genuinely "the moment this happens" (created_at, a transition instant, a relay's as-of) carries DEFAULT now() — a production caller omits it and the module supplies the time itself; the module never depends on the API to pass "now". Genuine business inputs stay required: issue_date, due_date, billing-period bounds, the gateway's own captured_at/paid_at, and each usage/measurement instant. The SQL test suite pins explicit timestamps so it stays deterministic; 039_test_now_defaults.psql proves the omit-and-default path.

Security

Test

SQL-suite green = the differential golden reports failed = 0 (1036/1036), every ok_* is t, and every negative case prints its ok: ER### line. After the SQL files, run-suite.sh runs the multi-database and multi-process tests in tests/shell/ (each creates and drops its own paytest_* databases on the same server; set SKIP_SHELL_TESTS=1 to skip) and exits non-zero if any strict test fails. They need PGBENCH (the complete pgbench command incl. host/user), and PAY_PHP_IMAGE (a PHP docker image with pdo_pgsql: docker build -t payments-php-pg -f tests/docker/php-pg.Dockerfile tests/docker) with PAY_VENDOR_DIR / PAY_FRAMEWORK_DIR / PAY_DB_* as described at the top of tests/shell/l7_2_webhook_inbox.sh; a missing prerequisite fails the run, it is never a silent skip.

tests/rehearsal/gateway_php_tests.sh runs the real-SQL PHP tests through a gateway's /call (typed JSON parameters, as a deployed platform does) instead of over PDO; the gateway keeps SQLSTATEs out of its HTTP answers, so those tests identify refusals by the SQL's own message there.

tests/rehearsal/gateway_migrate.sh drives a real, disposable StoneScriptDB Gateway through register / create / migrate on a fresh, a pre-existing and a partially seeded database, three migrate passes each. Never point it at a shared or production gateway.

License

MIT — see LICENSE.


All versions of stonescriptphp-payments with dependencies

PHP Build Version
Package Version
Requires php Version ^8.2
ext-json Version *
razorpay/razorpay Version ^2.9
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package progalaxyelabs/stonescriptphp-payments contains the following files

Loading the files please wait ...