Management Service
1. Overview
The Management Microservice is the backend for the Management Portal. It handles organization/location management, user management, reporting, configuration, and SAML SSO provider management. It validates Firebase Auth tokens and enforces RBAC.
Terminology note: the current gateway model is Organization -> Location. Legacy
/api/v1/merchants/*routes andmerchantIdfields remain as compatibility aliases for concrete location records, so this page keeps those wire examples where they still match the shipped API.Closeout note: treat the legacy
/merchantsroutes on this page as wire-level compatibility only. New product, portal, and SDK narratives should describe the management surface as organization/location-first, withmerchantIdexamples read as concrete location ids.
2. Responsibilities
| Responsibility | Description |
|---|---|
| Organization and location management | CRUD operations for organization/location profiles and configurations |
| User management | Create, update, disable users; manage roles via Firebase Custom Claims |
| SAML SSO administration | Configure and manage SAML identity providers in Firebase |
| Reporting | Generate transaction reports by querying the Processing Service |
| Audit logging | Record all administrative actions |
| Subscription oversight | View, search, and manage recurring subscriptions across organizations/locations |
| Configuration | Manage location-specific settings (branding, webhooks, etc.) |
2.1 Repo-side closeout guardrails
Safe statements:
- the service is organization/location-first in its tenant model
- legacy
/api/v1/merchants/*paths still exist as compatibility aliases - request and response examples that still show
merchantIdare referring to the concrete location record on the wire
Unsafe statements:
- that
/api/v1/merchants/*remains the preferred conceptual model for new integrations - that every management DTO has already renamed away from
merchantId - that legacy merchant-scoped examples imply a separate non-location tenant layer
2.2 Webhook Closeout Status
The webhook program in services/management is materially shipped across the
merchant-facing and operator-facing lifecycle:
| Area | Repo-side status |
|---|---|
Persist-before-publish (#594) | Landed: webhook events are durably stored before downstream fan-out. |
Delivery linkage + diagnostics (#595) | Landed: deliveries carry webhook-event linkage, bounded error detail, and allowlisted response headers. |
Retrieval APIs (#596) | Landed: management exposes list/get/deliveries retrieval for persisted webhook events. |
Single-event replay (#597) | Landed: replay uses the persisted event + original delivery set, with audit and rate-limit semantics. |
Bulk replay (#598) | Landed: admin async replay jobs, scheduler processing, completion summaries, and audit trails are present. |
Merchant Event Log (#600) | Landed: merchant-dashboard surfaces event log visibility, replay, and DLQ retry state. |
DLQ lifecycle (#601) | Landed: DLQ drain, retry, metrics, and alert/runbook wiring exist. |
Retention config + purge (#602) | Landed: per-organization retention configuration and purge orchestration exist. |
SDK/API contract surface (#603) | Landed: Kotlin/TypeScript SDK surfaces exist for retrieval, replay, retention, and bulk replay. |
Lifecycle integration coverage (#604) | Landed: integration coverage now exercises retry, replay, retrieval, and retention interactions together. |
The remaining webhook item is #605:
#605is not honest to call fully closed from the webhook lane alone.- Dedicated
organization_id/location_filterstorage exists on event subscriptions and management already uses those columns, but the issue is still coupled to the broader Organization/Location cutover and compatibility window. - Treat
#605as deferred follow-through under the ORG/location program, not as a missing webhook event-log/replay/lifecycle feature.
What is not left under #460 after the shipped webhook work above:
- a missing merchant event-log surface
- a missing replay or bulk-replay API
- a missing DLQ retry or retention operator path
- a missing lifecycle integration proof for replay/DLQ/retention interaction
- a missing Kotlin/TypeScript SDK surface for the management webhook APIs
2.3 #460 Closure Rule
Use the following rule when deciding whether to close the webhook umbrella:
| Condition | Honest status for #460 |
|---|---|
#460 means “all webhook event-storage/retrieval/replay lifecycle work except deferred ORG/location cutover follow-through” | Closable. |
#460 means “every listed child issue, including #605, must be fully complete” | Not closable. |
Practical guidance:
- close
#460only if the issue body or parent/child convention explicitly treats#605as deferred outside the webhook closeout lane - otherwise leave
#460open with#605as the only remaining blocker - do not reopen webhook replay/event-log/retention work under
#460unless a new concrete product or operator defect is found in the shipped surfaces
Closeout rule:
- If the parent webhook umbrella
#460requires every child issue to be fully closed, then#460is not yet honestly closable because#605remains deferred. - If
#460is being used operationally as the webhook events-storage umbrella and explicitly allows#605to stay deferred behind ORG cutover, then the product/operator webhook surfaces described on this page are otherwise in closure-grade shape.
3. API Specification
Authentication
All endpoints require a Firebase ID token:
Authorization: Bearer <firebase-id-token>
The service validates the token via Firebase Admin SDK and extracts custom claims for RBAC.
Endpoints
Merchant Management
POST /api/v1/merchants
GET /api/v1/merchants
GET /api/v1/merchants/{merchantId}
PUT /api/v1/merchants/{merchantId}
PATCH /api/v1/merchants/{merchantId}/status
POST /api/v1/merchants
Authorization: Bearer <token>
Request:
{
"businessName": "Acme Vape Shop",
"dba": "Acme Vapes",
"businessType": "retail",
"mcc": "5993",
"contactName": "Jane Doe",
"contactEmail": "jane@acmevapes.com",
"contactPhone": "555-0123",
"address": {
"street": "123 Main St",
"city": "Charlotte",
"state": "NC",
"zip": "28202"
},
"transitConfig": {
"mid": "<TSYS Merchant ID>",
"tid": "<TSYS Terminal ID>",
"industryType": "RE"
},
"branding": {
"logoUrl": "https://...",
"primaryColor": "#1a73e8"
},
"webhookUrl": "https://acmevapes.com/webhooks/payment"
}
Response (201):
{
"merchantId": "loc_abc123",
"businessName": "Acme Vape Shop",
"status": "ACTIVE",
"createdAt": "2026-03-03T15:00:00Z",
...
}
Read the merchantId above as the concrete location id carried by the current
wire contract, not as a different top-level tenant type from Location.
Merchant Activation (TransIT TID)
POST /api/v1/merchants/{merchantId}/activate-transit
Request:
{
"transitMid": "<TSYS Merchant ID>",
"transitTid": "<TSYS Terminal ID>"
}
Response (200):
{
"merchantId": "loc_abc123",
"transitActivationStatus": "ACTIVE",
"activatedAt": "2026-03-03T15:00:00Z"
}
Simplified merchant activation — stores TID/MID configuration and activates the merchant in our system. The support team enters credentials obtained from the TransIT onboarding process. No external portal dependency.
User Management
POST /api/v1/users
GET /api/v1/users
GET /api/v1/users/{userId}
PUT /api/v1/users/{userId}
DELETE /api/v1/users/{userId}
POST /api/v1/users
Request:
{
"email": "john@acmevapes.com",
"displayName": "John Smith",
"role": "merchant_user",
"merchantIds": ["loc_abc123"]
}
Response (201):
{
"userId": "firebase-uid-xyz",
"email": "john@acmevapes.com",
"role": "merchant_user",
"status": "ACTIVE",
"createdAt": "2026-03-03T15:00:00Z"
}
Read the role and merchantIds fields above as current compatibility shape:
the legacy role names and merchantIds list still point at
organization/location-scoped access, not a separate merchant-first tenant model.
Docs-side closeout checklist
Treat this page as closeout-ready from the repo side only if readers can infer all of the following without code archaeology:
- organization/location is the primary mental model
- legacy
/merchantsroutes are compatibility aliases, not the preferred narrative - Firebase claims and API payloads may still surface
merchantId-style fields that point at concrete location records - legacy role names such as
merchant_adminandmerchant_userare compatibility labels for location-scoped access
SAML SSO Configuration
POST /api/v1/saml-providers # super_admin only
GET /api/v1/saml-providers
GET /api/v1/saml-providers/{id}
PUT /api/v1/saml-providers/{id}
DELETE /api/v1/saml-providers/{id}
POST /api/v1/saml-providers/{id}/test
POST /api/v1/saml-providers
Request:
{
"providerId": "saml.acme-corp",
"displayName": "Acme Corp SSO",
"idpEntityId": "https://idp.acmecorp.com/saml/metadata",
"ssoUrl": "https://idp.acmecorp.com/saml/sso",
"x509Certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
"rpEntityId": "pinpoint-gateway",
"merchantIds": ["loc_abc123"]
}
Reporting (Proxied from Processing Service)
GET /api/v1/transactions?merchantId=...&from=...&to=...
GET /api/v1/transactions/{transactionId}
POST /api/v1/transactions/{transactionId}/void
POST /api/v1/transactions/{transactionId}/refund
POST /api/v1/transactions/manual
GET /api/v1/settlements?merchantId=...&date=...
GET /api/v1/reports/daily?merchantId=...&date=...
GET /api/v1/reports/monthly?merchantId=...&month=...
GET /api/v1/reports/custom?merchantId=...&from=...&to=...
GET /api/v1/reports/export?format=csv&...
GET /api/v1/reports/card-brand?merchantId=...&from=...&to=...
Reports are available via API call (not just portal UI). Support breakdowns by 4 major card brands (Visa, Mastercard, Amex, Discover). Export: CSV, PDF.
RESOLVED: Test merchant accounts are managed entirely within our Management Portal. No dependency on TSYS Merchant Center — we handle merchant lifecycle, batch management, and reporting ourselves.
Subscription Management (Admin Portal)
Proxied from Processing Microservice. Provides admin visibility into all recurring billing across merchants.
GET /api/v1/subscriptions?merchantId=...&status=...&from=...&to=...
GET /api/v1/subscriptions/{subscriptionId}
GET /api/v1/subscriptions/{subscriptionId}/billing-history
POST /api/v1/subscriptions/{subscriptionId}/cancel # Admin force-cancel
POST /api/v1/subscriptions/{subscriptionId}/resume # Admin force-resume
GET /api/v1/reports/subscriptions?merchantId=...&from=...&to=...
GET /api/v1/reports/subscriptions/churn?merchantId=...&from=...&to=...
GET /api/v1/reports/subscriptions/mrr?merchantId=...&from=...&to=...
| Report | Description |
|---|---|
| Subscription list | All subscriptions for a merchant, filterable by status, date, customer |
| Billing history | Per-subscription charge history with success/failure details |
| Churn report | Cancellations and suspensions over time period |
| MRR report | Monthly Recurring Revenue aggregation by merchant |
RBAC: super_admin and admin can view/manage all subscriptions.
merchant_admin can view/manage subscriptions for the caller's assigned
organization/location scope. merchant_user and readonly can view only.
Audit Log
GET /api/v1/audit-log?userId=...&action=...&from=...&to=...
Response:
{
"entries": [
{
"id": "audit_001",
"userId": "firebase-uid-xyz",
"userEmail": "admin@pinpoint.com",
"action": "MERCHANT_CREATED",
"resourceType": "merchant",
"resourceId": "loc_abc123",
"details": { "businessName": "Acme Vape Shop" },
"ipAddress": "203.0.113.42",
"timestamp": "2026-03-03T15:00:00Z"
}
],
"total": 156,
"limit": 50,
"offset": 0
}
4. Technical Stack
| Component | Technology |
|---|---|
| Language | Kotlin (JVM 25) |
| Framework | Spring Boot 4.0.2 |
| Auth | Firebase Admin SDK (Kotlin) |
| Database | Cloud Spanner (PostgreSQL dialect) — merchants, users schemas |
| Build | Bazel |
| Testing | JUnit 5, Testcontainers, MockMvc |
5. Database Schema
CREATE TABLE merchants (
id VARCHAR(64) PRIMARY KEY,
business_name VARCHAR(255) NOT NULL,
dba VARCHAR(255),
business_type VARCHAR(50),
mcc VARCHAR(4),
contact_name VARCHAR(255),
contact_email VARCHAR(255),
contact_phone VARCHAR(20),
address_street VARCHAR(255),
address_city VARCHAR(100),
address_state VARCHAR(2),
address_zip VARCHAR(10),
transit_mid VARCHAR(64),
transit_tid VARCHAR(64),
industry_type VARCHAR(4) DEFAULT 'RE',
logo_url VARCHAR(512),
primary_color VARCHAR(7),
webhook_url VARCHAR(512),
webhook_secret VARCHAR(128),
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE portal_users (
id VARCHAR(128) PRIMARY KEY, -- Firebase UID
email VARCHAR(255) NOT NULL UNIQUE,
display_name VARCHAR(255),
role VARCHAR(30) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE user_merchant_access (
user_id VARCHAR(128) REFERENCES portal_users(id),
merchant_id VARCHAR(64) REFERENCES merchants(id),
PRIMARY KEY (user_id, merchant_id)
);
CREATE TABLE audit_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id VARCHAR(128),
user_email VARCHAR(255),
action VARCHAR(50) NOT NULL,
resource_type VARCHAR(30),
resource_id VARCHAR(64),
details JSONB,
ip_address INET,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_audit_log_user ON audit_log(user_id, created_at DESC);
CREATE INDEX idx_audit_log_action ON audit_log(action, created_at DESC);
6. RBAC Enforcement
// Firebase custom claims structure
mapOf(
"role" to "merchant_admin",
"merchantAccess" to listOf(
mapOf("m" to "loc_abc123", "r" to "merchant_admin")
)
)
Permissions are not stored in Firebase custom claims. They are derived server-side from the
platform role and merchantAccess entries, and exposed to the portal through the /api/v1/me
response when needed.
| Endpoint | super_admin | admin | merchant_admin | merchant_user | readonly |
|---|---|---|---|---|---|
| Manage SAML | Yes | No | No | No | No |
| Manage all merchants | Yes | Yes | No | No | No |
| Manage own merchant | Yes | Yes | Yes | No | No |
| View all transactions | Yes | Yes | No | No | No |
| View own transactions | Yes | Yes | Yes | Yes | Yes |
| Void/refund | Yes | Yes | Yes | No | No |
| Manage users | Yes | Yes | Yes (own merchant) | No | No |
| View all subscriptions | Yes | Yes | No | No | No |
| View own subscriptions | Yes | Yes | Yes | Yes | Yes |
| Cancel/resume subscriptions | Yes | Yes | Yes (own merchant) | No | No |
| Subscription reports (MRR, churn) | Yes | Yes | Yes (own merchant) | No | No |
| View audit log | Yes | Yes | No | No | No |
7. Health Check
GET /actuator/health
Response:
{
"status": "UP",
"components": {
"db": { "status": "UP" },
"firebase": { "status": "UP" }
}
}