Skip to main content

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 and merchantId fields 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 /merchants routes on this page as wire-level compatibility only. New product, portal, and SDK narratives should describe the management surface as organization/location-first, with merchantId examples read as concrete location ids.


2. Responsibilities

ResponsibilityDescription
Organization and location managementCRUD operations for organization/location profiles and configurations
User managementCreate, update, disable users; manage roles via Firebase Custom Claims
SAML SSO administrationConfigure and manage SAML identity providers in Firebase
ReportingGenerate transaction reports by querying the Processing Service
Audit loggingRecord all administrative actions
Subscription oversightView, search, and manage recurring subscriptions across organizations/locations
ConfigurationManage 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 merchantId are 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:

AreaRepo-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:

  • #605 is not honest to call fully closed from the webhook lane alone.
  • Dedicated organization_id / location_filter storage 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 #605 as 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:

ConditionHonest 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 #460 only if the issue body or parent/child convention explicitly treats #605 as deferred outside the webhook closeout lane
  • otherwise leave #460 open with #605 as the only remaining blocker
  • do not reopen webhook replay/event-log/retention work under #460 unless a new concrete product or operator defect is found in the shipped surfaces

Closeout rule:

  • If the parent webhook umbrella #460 requires every child issue to be fully closed, then #460 is not yet honestly closable because #605 remains deferred.
  • If #460 is being used operationally as the webhook events-storage umbrella and explicitly allows #605 to 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 /merchants routes 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_admin and merchant_user are 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=...
ReportDescription
Subscription listAll subscriptions for a merchant, filterable by status, date, customer
Billing historyPer-subscription charge history with success/failure details
Churn reportCancellations and suspensions over time period
MRR reportMonthly 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

ComponentTechnology
LanguageKotlin (JVM 25)
FrameworkSpring Boot 4.0.2
AuthFirebase Admin SDK (Kotlin)
DatabaseCloud Spanner (PostgreSQL dialect) — merchants, users schemas
BuildBazel
TestingJUnit 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.

Endpointsuper_adminadminmerchant_adminmerchant_userreadonly
Manage SAMLYesNoNoNoNo
Manage all merchantsYesYesNoNoNo
Manage own merchantYesYesYesNoNo
View all transactionsYesYesNoNoNo
View own transactionsYesYesYesYesYes
Void/refundYesYesYesNoNo
Manage usersYesYesYes (own merchant)NoNo
View all subscriptionsYesYesNoNoNo
View own subscriptionsYesYesYesYesYes
Cancel/resume subscriptionsYesYesYes (own merchant)NoNo
Subscription reports (MRR, churn)YesYesYes (own merchant)NoNo
View audit logYesYesNoNoNo

7. Health Check

GET /actuator/health

Response:
{
"status": "UP",
"components": {
"db": { "status": "UP" },
"firebase": { "status": "UP" }
}
}