10. Collection metrics and chart data
10.1 Summary/chart endpoints and filters
| Method and path | Access | Status and result |
|---|---|---|
GET /portal/collections/metrics |
Active Administrator or Developer, user PAT with merchant-portal scope |
200, gross collection totals, counts, and success rate |
GET /portal/collections/charts |
Active Administrator or Developer, user PAT with merchant-portal scope |
ordered daily/monthly/yearly buckets and reconciled overall totals |
Summary and charts share these population filters:
| Query | Contract |
|---|---|
environment |
test or live |
from, to |
Required valid ISO 8601 instants including seconds and explicit Z/offset; up to six fractional-second digits; start-inclusive/end-exclusive; from < to |
currency |
Required supported three-letter uppercase currency from the local catalogue; no mixing currencies |
timezone |
Optional IANA timezone; default Asia/Manila; numeric offsets are rejected |
payment_brand |
Optional nonempty string, maximum 100 characters; equality filter matching the transaction list contract |
interval |
Required for charts: day, month, or year; rejected by summary endpoint |
Both endpoints echo the effective timezone and validated input range. Filtering compares explicit from/to instants after converting them to the application's database timestamp timezone (config('app.timezone')); changing the reporting timezone does not reinterpret the same instants. The ten-calendar-year range cap is measured from from in the reporting timezone, clipping a leap-day anniversary to the last valid calendar day. Named reporting timezones and defaults are independent of the workstation timezone. Chart boundaries follow the reporting timezone, including daylight-saving changes.
The maximum range is ten calendar years. Unknown fields, missing/invalid filters, equal/reversed instants, and excessive ranges return 422 VALIDATION_ERROR. No pagination, status selector, merchant selector, search query, or as_of input is supported. Merchant context is resolved from the authenticated user: a foreign merchant selector is denied with 403 MERCHANT_SCOPE_MISMATCH. Invalid/missing currency catalogue returns 503 SERVICE_UNAVAILABLE. Both endpoints require the Portal user PAT and enforce active membership, application eligibility and scope checks. Successful responses use Cache-Control: no-store.
Request (no body):
GET /api/v1/portal/collections/metrics?environment=live¤cy=PHP&from=2026-09-01T00%3A00%3A00%2B08%3A00&to=2026-10-01T00%3A00%3A00%2B08%3A00&timezone=Asia%2FManila
Authorization: Bearer <user_pat>
Accept: application/json
``` URL-encode `+` as `%2B`.
Charts permit at most 366 daily, 120 monthly, or 10 yearly buckets per chart request, returning `422` with an `interval` field error when exceeded. Limits count all intersected calendar periods, including partial and empty buckets. A ten-year range beginning/ending mid-year can therefore exceed the ten-yearly-bucket limit. Requests are never silently truncated or assigned a different interval. Partial first/last buckets are clipped and marked `partial=true`; empty buckets have zero totals/counts and null success rate.
### 10.2 Metric definitions {#102-metric-definitions}
Metrics count stored collection transactions once across providers, scoped by merchant, environment, currency, optional brand and **creation time** in `[from,to)`. Archived/credited records remain included. Current normalized status determines the result, so historical reports can change as pending transactions resolve. These are creation-cohort metrics, not settlement-date accounting.
| Field | Definition |
| --- | --- |
| `transaction_count` | Number of transactions in the population |
| `successful_count` | Current normalized status `PAID` |
| `pending_count` | Current normalized status `PENDING` |
| `rejected_count` | Current normalized status `REJECTED` |
| `collection_total` | Sum of transaction amounts for `PAID` transactions in the population |
| `success_rate_percent` | `successful_count / transaction_count * 100`, rounded to 2 decimal places; null when count is zero |
Pending and rejected transactions are included in the success-rate denominator. The three status counts must add up to `transaction_count`. This deliberately defines one denominator; do not substitute successful/terminal-only counts without a separately named metric. Return percentage values as decimal strings, for example `"85.00"`.
Collection total is gross successful collection amount before fees; it is not net settlement or refundable balance. Refund/reversal accounting is outside this version. Do not infer success from HTTP status, webhook acknowledgement, non-empty payment messages, or the existing legacy resource's default status alone.
Each summary uses one consistent database read. `as_of` is a UTC observation marker, not a historical filter or reusable snapshot. Separate HTTP reads can observe different states.
Amounts and rates are decimal strings with two fractional digits. Empty populations return `collection_total:"0.00"`, zero counts and `success_rate_percent:null`; nonempty populations without successes return rate `"0.00"`. Paid records with null amounts return `503 SERVICE_UNAVAILABLE` for the entire summary/chart. Pending/rejected null amounts still count.
Chart totals reconcile with their buckets. Overall rates use overall counts, never an average of bucket percentages. Separate summary/chart requests require unchanged data for exact reconciliation; a paid null amount withholds the entire chart with `503`.
**metrics response (synthetic values):**
```json
{
"success": true,
"message": "Success",
"data": {
"environment": "live",
"currency": "PHP",
"timezone": "Asia/Manila",
"from": "2026-09-01T00:00:00+08:00",
"to": "2026-10-01T00:00:00+08:00",
"as_of": "2026-10-01T01:00:00Z",
"date_basis": "created_at",
"collection_total": "127500.00",
"transaction_count": 100,
"successful_count": 85,
"pending_count": 10,
"rejected_count": 5,
"success_rate_percent": "85.00"
}
}
10.3 Daily, monthly, and yearly charts
| View | Example filters | Bucket label |
|---|---|---|
| Daily | interval=day, September 1 through October 1 |
2026-09-01, 2026-09-02, … |
| Monthly | interval=month, January 1, 2026 through January 1, 2027 |
2026-01, 2026-02, … |
| Yearly | interval=year, January 1, 2024 through January 1, 2027 |
2024, 2025, 2026 |
Dates in this table describe local calendar boundaries; send full timestamps with offsets in the actual query. URL-encode + as %2B. Every chart bucket includes all six metrics, allowing amount, count, and success-rate charts from the same response.
Each bucket represents a local calendar day, month or year, clipped to [from,to). period identifies that calendar period; bucket from/to use the reporting timezone's actual offsets and include six fractional-second digits. An exclusive to exactly at a boundary does not create an extra empty period. Day boundaries use local midnight, month boundaries use the first day, and year boundaries use January 1. Day length may be 23 or 25 hours during daylight-saving changes. Changing timezone may change labels/bucket counts while retaining the same overall population.
Request (no body):
GET /api/v1/portal/collections/charts?environment=test¤cy=PHP&interval=day&from=2026-09-01T00%3A00%3A00%2B08%3A00&to=2026-09-02T00%3A00%3A00%2B08%3A00&timezone=Asia%2FManila
Authorization: Bearer <user_pat>
Accept: application/json
one-day chart example (synthetic values):
{
"success": true,
"message": "Success",
"data": {
"environment": "test",
"currency": "PHP",
"timezone": "Asia/Manila",
"interval": "day",
"date_basis": "created_at",
"from": "2026-09-01T00:00:00+08:00",
"to": "2026-09-02T00:00:00+08:00",
"as_of": "2026-10-01T01:00:00Z",
"totals": {
"collection_total": "1500.00",
"transaction_count": 3,
"successful_count": 1,
"pending_count": 1,
"rejected_count": 1,
"success_rate_percent": "33.33"
},
"buckets": [
{
"period": "2026-09-01",
"from": "2026-09-01T00:00:00.000000+08:00",
"to": "2026-09-02T00:00:00.000000+08:00",
"partial": false,
"collection_total": "1500.00",
"transaction_count": 3,
"successful_count": 1,
"pending_count": 1,
"rejected_count": 1,
"success_rate_percent": "33.33"
}
]
}
}
Return null success rates for empty buckets; the frontend can show “No transactions.” Zero percent means transactions exist and none are successful.
10.4 Request validation reference
These tables reflect the current implementation’s request rules. See input conventions for required/optional/null behavior and error formats.
Validation: GET /api/v1/portal/collections/metrics
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
environment |
Query | Required | String; exactly test or live, case-sensitive. |
currency |
Query | Required | String; exactly three uppercase ASCII letters and present in DLT’s currency catalogue. Unsupported code returns 422; unavailable catalogue returns 503. |
from |
Query | Required | String in YYYY-MM-DDTHH:mm:ss[.ffffff]Z or YYYY-MM-DDTHH:mm:ss[.ffffff]±HH:MM format; real calendar date/time, nonzero year, seconds required, optional 1–6 fractional digits, offset up to ±14:00. Date-only, offsetless and impossible instants are invalid. |
to |
Query | Required | String in YYYY-MM-DDTHH:mm:ss[.ffffff]Z or YYYY-MM-DDTHH:mm:ss[.ffffff]±HH:MM format; real calendar date/time, nonzero year, seconds required, optional 1–6 fractional digits, offset up to ±14:00. Date-only, offsetless and impossible instants are invalid. Strictly later than from; interval must not exceed ten calendar years measured from from in the reporting timezone. |
timezone |
Query | Optional; default Asia/Manila | Nonempty string; recognized PHP timezone identifier (Laravel timezone validation), such as Asia/Manila; not an arbitrary UTC-offset string. |
payment_brand |
Query | Optional; required if supplied | Nonempty string, maximum 100 characters; exact stored-brand match. |
Send no request body. Use query parameters; null/empty optional values are invalid. Unlisted payload/query fields are rejected with 422 VALIDATION_ERROR. A conflicting merchant selector may instead be rejected earlier with 403 MERCHANT_SCOPE_MISMATCH. interval, pagination, status and merchant-reference filters are not accepted.
Validation: GET /api/v1/portal/collections/charts
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
environment |
Query | Required | String; exactly test or live, case-sensitive. |
currency |
Query | Required | String; exactly three uppercase ASCII letters and present in DLT’s currency catalogue. Unsupported code returns 422; unavailable catalogue returns 503. |
from |
Query | Required | String in YYYY-MM-DDTHH:mm:ss[.ffffff]Z or YYYY-MM-DDTHH:mm:ss[.ffffff]±HH:MM format; real calendar date/time, nonzero year, seconds required, optional 1–6 fractional digits, offset up to ±14:00. Date-only, offsetless and impossible instants are invalid. |
to |
Query | Required | String in YYYY-MM-DDTHH:mm:ss[.ffffff]Z or YYYY-MM-DDTHH:mm:ss[.ffffff]±HH:MM format; real calendar date/time, nonzero year, seconds required, optional 1–6 fractional digits, offset up to ±14:00. Date-only, offsetless and impossible instants are invalid. Strictly later than from; interval must not exceed ten calendar years measured from from in the reporting timezone. |
timezone |
Query | Optional; default Asia/Manila | Nonempty string; recognized PHP timezone identifier (Laravel timezone validation), such as Asia/Manila; not an arbitrary UTC-offset string. |
payment_brand |
Query | Optional; required if supplied | Nonempty string, maximum 100 characters; exact stored-brand match. |
interval |
Query | Required | String; exactly day, month or year. Maximum generated calendar buckets: 366 daily, 120 monthly, 10 yearly, including partial periods. |
Send no request body. Same ten-calendar-year bound as metrics, plus bucket count in the reporting timezone. Excess buckets return a 422 interval field error. Unlisted payload/query fields are rejected with 422 VALIDATION_ERROR. A conflicting merchant selector may instead be rejected earlier with 403 MERCHANT_SCOPE_MISMATCH.