Merchant API — Implementation Specification
Microservice: merchant-api
Package: com.myriad.merchant_api
IcePanel Group: End-user Microservice (HcYURlY2KWJDy4HUmPl0)
IcePanel Landscape: View Architecture
0. Scope & Tech Stack
0.1 Scope
This microservice implements the End-user Microservice group from the C4 architecture. It serves all organization-owner, org-admin, store-admin, store-manager, and store-member endpoints — everything a merchant and their staff interact with.
In scope:
- Public routes (health, login, invite validation/acceptance, PIN login)
- Authenticated routes (me, change-password)
- Organization-scoped routes (members, customers, categories, suppliers, serial numbers, notifications, promotions, stock transfers, invites, activity log)
- Store-scoped routes (members, products, inventory, transactions, discounts, returns, shifts, compliance, tax configs, terminals, settings, onboarding, reports, query, mKonnekt, invites, activity log)
Out of scope (belong to management-api):
POST /api/v1/platform/auth/login— platform admin loginGET /api/v1/platform/auth/me— platform admin profile- All
/api/v1/platform/*routes — org CRUD, user CRUD, impersonation - Customer Support API routes
0.2 Endpoint Count
| Scope | Count |
|---|---|
| Public (no auth) | 5 |
| Authenticated (JWT required) | 2 |
| Organization-scoped | 55 |
| Store-scoped | 93 |
| Total | 155 |
0.3 Tech Stack
| Layer | Technology |
|---|---|
| Language | Java 25 (--release 25), no Lombok |
| Framework | Spring Boot 4.0.0 / Spring Framework 7.0 (HTTP/REST, validation, security) |
| Database | Google Cloud Spanner (PostgreSQL interface) |
| Local DB | PostgreSQL 15+ (localhost:5432/pinpoint) |
| Schema & Data Layer | SQLDelight — .sq files generate shared Kotlin types |
| Shared Types | kt_jvm_library consumed by Android, terminal-api, merchant-api via Kotlin-Java bytecode interop |
| Auth | JJWT 0.12.5, BCrypt (Spring Security) |
| Build | Bazel 9.0.0 (bzlmod), Maven deps under @maven_spring// |
| Container | OCI image, distroless Java 25 base, port 8080 |
| IAM | Allow-all stub (Dafny spec at specs/playground/iam.dfy not finalized) |
0.4 Existing Boilerplate (9 files)
| File | Status |
|---|---|
CoreApplication.java | Keep — Spring Boot entry point |
config/JpaConfig.java | Remove — replaced by SQLDelight DatabaseConfig |
controller/HealthController.java | Keep — GET /health |
dto/ApiResponse.java | Keep — response envelope record |
entity/BaseEntity.java | Remove — replaced by SQLDelight-generated types |
exception/GlobalExceptionHandler.java | Keep — centralized error handling |
exception/ResourceNotFoundException.java | Keep |
exception/UnauthorizedException.java | Keep |
exception/ForbiddenException.java | Keep |
0.5 Schema Location
SQLDelight .sq files live in apps/specifications/schema/ and are shared across:
apps/specifications/schema/*.sq → SQLDelight generates → kt_jvm_library (shared)
│
┌─────────────────────────────────────────┼──────────────────┐
Android app merchant-api (Java) terminal-api (Java)
(Kotlin native) (bytecode interop) (bytecode interop)
0.6 Context Path Note
application.yml sets context-path: /api/v1. All controller @RequestMapping paths are relative — Spring prepends the context path automatically. The health endpoint is therefore served at /api/v1/health, not /health.
1. Architecture Overview
1.1 IcePanel Component Map
Each IcePanel C3 component maps to one or more Java/Kotlin packages within com.myriad.merchant_api:
| IcePanel Component | ID | Java/Kotlin Mapping |
|---|---|---|
| JWT Validator & User Locator | 2qFJqSyc7PcVuv3SzjqU | security.filter.JwtAuthenticationFilter, security.service.JwtService, security.service.UserLocatorService |
| End-User Security Module | oh3tdWdLe307VoL8QU8y | security.filter.OrgContextFilter, security.filter.StoreContextFilter, security.context.OrgContext, security.context.StoreContext |
| Formal Verified IAM Module | gU2rWxgSFybR0v4XKhGd | iam.IamService (allow-all stub), iam.IamDecisionEngine (Phase 9) |
| Auth Wrapper | nWvdC16eDje1WXE3Nd4M | security.service.AuthWrapperService (BCrypt hashing, token generation) |
| Authentication API Routes | gDjNSTJnNH1pA0opsvnp | controller.auth.AuthController, controller.auth.PinAuthController |
| Organization API Routes | qTu7Ci4UU3DLEyin3Me0 | controller.org.* (all org-scoped controllers) |
| Store API Routes | 4LyRc699nG863bDes5jb | controller.store.* (all store-scoped controllers) |
| User REST/CRUD interface | aElkzEYQoZLWZIHq6uVR | service.UserService, repository.UserRepository |
1.2 Security Data Flow
┌─────────────────────┐
│ Main Load Balancer │
└─────────┬───────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
Public Routes JWT Validator & (management-api
(no filter) User Locator — not us)
│
▼
End-User Security
Module
╱ ╲
checks╱ ╲sets context
╱ ╲
IAM Module OrgContext /
(allow-all) StoreContext
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
Auth API Routes Org API Routes Store API Routes
│ │ │
└────────────────────┼────────────────────┘
▼
POS SQL Database
(Spanner)
Request lifecycle:
- Load balancer routes to merchant-api
JwtAuthenticationFilterextractsAuthorization: Bearer <token>, validates via JJWT, resolves user from DB, setsSecurityContextOrgContextFilterextracts:org_idfrom path, validates user's org membership, setsOrgContextStoreContextFilterextracts:store_idfrom path, validates user's store membership, setsStoreContext- Controller method executes with authenticated user + org/store context available
IamService.checkAccess()called (currently returnstrue— allow-all stub)
1.3 Data Layer Architecture
SQLDelight replaces JPA/Hibernate as the data access layer. The schema is defined once in .sq files and shared across all consumers.
How it works:
apps/specifications/schema/
├── migrations/
│ ├── 1.sqm -- CREATE TABLE users ...
│ ├── 2.sqm -- CREATE TABLE organizations ...
│ └── ...
├── User.sq -- Named queries for users table
├── Organization.sq -- Named queries for organizations table
├── Store.sq -- Named queries for stores table
└── ...
SQLDelight generates from each .sq file:
- Kotlin data class — e.g.,
User(userId: UUID, email: String, ...)— the row type - Query interface — e.g.,
UserQueries.findById(id: UUID): Query<User>— type-safe queries - Database interface — aggregates all query interfaces
Java consumption (merchant-api):
// SQLDelight-generated Kotlin type, used from Java seamlessly
User user = database.getUserQueries().findById(userId).executeAsOne();
String email = user.getEmail(); // Kotlin getter, works in Java
UUID id = user.getUserId(); // UUID type preserved
Why not JPA/Hibernate:
- Single schema source-of-truth across Android (Kotlin), terminal-api (Java), merchant-api (Java)
- No ORM magic — explicit SQL, predictable queries
- Spanner-compatible —
.sqfiles use PostgreSQL dialect that Spanner understands - Type-safe — compile-time verification of queries against schema
1.4 Complete Package Tree
com.myriad.merchant_api
├── CoreApplication.java [EXISTS]
│
├── config/
│ ├── DatabaseConfig.java [Phase 0a] -- SQLDelight JDBC setup
│ ├── SecurityConfig.java [Phase 0b] -- filter chain, CORS
│ └── CorsConfig.java [Phase 0b]
│
├── security/
│ ├── filter/
│ │ ├── JwtAuthenticationFilter.java [Phase 0b]
│ │ ├── OrgContextFilter.java [Phase 0b]
│ │ └── StoreContextFilter.java [Phase 0b]
│ ├── context/
│ │ ├── SecurityContext.java [Phase 0b]
│ │ ├── OrgContext.java [Phase 0b]
│ │ └── StoreContext.java [Phase 0b]
│ └── service/
│ ├── JwtService.java [Phase 0b]
│ ├── UserLocatorService.java [Phase 0b]
│ └── AuthWrapperService.java [Phase 1]
│
├── iam/
│ ├── IamService.java [Phase 0b] -- allow-all stub
│ └── IamDecisionEngine.java [Phase 9]
│
├── controller/
│ ├── HealthController.java [EXISTS]
│ ├── auth/
│ │ ├── AuthController.java [Phase 1]
│ │ └── PinAuthController.java [Phase 1]
│ ├── invite/
│ │ └── InviteController.java [Phase 1]
│ ├── org/
│ │ ├── OrgMemberController.java [Phase 2]
│ │ ├── OrgCustomerController.java [Phase 7]
│ │ ├── CategoryController.java [Phase 3]
│ │ ├── SupplierController.java [Phase 3]
│ │ ├── OrgSerialNumberController.java [Phase 4]
│ │ ├── OrgNotificationController.java [Phase 7]
│ │ ├── OrgPromotionController.java [Phase 7]
│ │ ├── OrgStockTransferController.java [Phase 7]
│ │ ├── OrgInviteController.java [Phase 1]
│ │ └── OrgActivityLogController.java [Phase 7]
│ └── store/
│ ├── StoreMemberController.java [Phase 2]
│ ├── ProductController.java [Phase 3]
│ ├── InventoryController.java [Phase 4]
│ ├── TransactionController.java [Phase 5]
│ ├── DiscountController.java [Phase 5]
│ ├── ReturnController.java [Phase 5]
│ ├── ShiftController.java [Phase 6]
│ ├── ComplianceController.java [Phase 6]
│ ├── TaxConfigController.java [Phase 6]
│ ├── TerminalController.java [Phase 6]
│ ├── SettingsController.java [Phase 6]
│ ├── OnboardingController.java [Phase 6]
│ ├── StoreInviteController.java [Phase 1]
│ ├── StoreActivityLogController.java [Phase 7]
│ ├── ReportController.java [Phase 8]
│ ├── QueryController.java [Phase 8]
│ └── MkonnektController.java [Phase 8]
│
├── service/
│ ├── AuthService.java [Phase 1]
│ ├── InviteService.java [Phase 1]
│ ├── UserService.java [Phase 1]
│ ├── OrgMemberService.java [Phase 2]
│ ├── StoreMemberService.java [Phase 2]
│ ├── ProductService.java [Phase 3]
│ ├── CategoryService.java [Phase 3]
│ ├── SupplierService.java [Phase 3]
│ ├── InventoryService.java [Phase 4]
│ ├── SerialNumberService.java [Phase 4]
│ ├── TransactionService.java [Phase 5]
│ ├── ReturnService.java [Phase 5]
│ ├── DiscountService.java [Phase 5]
│ ├── ShiftService.java [Phase 6]
│ ├── ComplianceService.java [Phase 6]
│ ├── TaxConfigService.java [Phase 6]
│ ├── TerminalService.java [Phase 6]
│ ├── SettingsService.java [Phase 6]
│ ├── OnboardingService.java [Phase 6]
│ ├── CustomerService.java [Phase 7]
│ ├── PromotionService.java [Phase 7]
│ ├── NotificationService.java [Phase 7]
│ ├── StockTransferService.java [Phase 7]
│ ├── ActivityLogService.java [Phase 7]
│ ├── ReportService.java [Phase 8]
│ ├── QueryService.java [Phase 8]
│ └── MkonnektService.java [Phase 8]
│
├── repository/
│ │ (Thin wrappers around SQLDelight-generated query interfaces.
│ │ Each injects the shared Database and delegates to the
│ │ appropriate *Queries object.)
│ ├── UserRepository.java [Phase 1]
│ ├── InviteRepository.java [Phase 1]
│ ├── OrganizationRepository.java [Phase 2]
│ ├── StoreRepository.java [Phase 2]
│ ├── OrgMembershipRepository.java [Phase 2]
│ ├── StoreMembershipRepository.java [Phase 2]
│ ├── ProductRepository.java [Phase 3]
│ ├── CategoryRepository.java [Phase 3]
│ ├── SupplierRepository.java [Phase 3]
│ ├── ProductSupplierRepository.java [Phase 3]
│ ├── InventoryRepository.java [Phase 4]
│ ├── LedgerRepository.java [Phase 4]
│ ├── SerialNumberRepository.java [Phase 4]
│ ├── TransactionRepository.java [Phase 5]
│ ├── ReturnRepository.java [Phase 5]
│ ├── DiscountRepository.java [Phase 5]
│ ├── ShiftRepository.java [Phase 6]
│ ├── ComplianceRepository.java [Phase 6]
│ ├── TaxConfigRepository.java [Phase 6]
│ ├── TerminalRepository.java [Phase 6]
│ ├── CustomerRepository.java [Phase 7]
│ ├── PromotionRepository.java [Phase 7]
│ ├── NotificationRepository.java [Phase 7]
│ ├── StockTransferRepository.java [Phase 7]
│ └── ActivityLogRepository.java [Phase 7]
│
├── dto/
│ ├── ApiResponse.java [EXISTS]
│ ├── request/ [Phase 1+]
│ │ ├── LoginRequest.java
│ │ ├── ChangePasswordRequest.java
│ │ ├── PinLoginRequest.java
│ │ ├── ValidateInviteRequest.java
│ │ ├── AcceptInviteRequest.java
│ │ ├── CreateInviteRequest.java
│ │ ├── AddOrgMemberRequest.java
│ │ ├── UpdateOrgMemberRoleRequest.java
│ │ ├── AddStoreMemberRequest.java
│ │ ├── UpdateStoreMemberRoleRequest.java
│ │ ├── CreateProductRequest.java
│ │ ├── UpdateProductRequest.java
│ │ ├── CreateCategoryRequest.java
│ │ ├── UpdateCategoryRequest.java
│ │ ├── CreateSupplierRequest.java
│ │ ├── UpdateSupplierRequest.java
│ │ ├── AddProductSupplierRequest.java
│ │ ├── ReceiveInventoryRequest.java
│ │ ├── AdjustInventoryRequest.java
│ │ ├── TransferInventoryRequest.java
│ │ ├── CreateSerialNumberRequest.java
│ │ ├── CreateReturnRequest.java
│ │ ├── CreateDiscountRequest.java
│ │ ├── UpdateDiscountRequest.java
│ │ ├── CreateShiftPreferenceRequest.java
│ │ ├── UpdateShiftPreferenceRequest.java
│ │ ├── CreateShiftAssignedRequest.java
│ │ ├── UpdateShiftAssignedRequest.java
│ │ ├── CreateShiftRequestRequest.java
│ │ ├── ReviewShiftRequestRequest.java
│ │ ├── VerifyAgeRequest.java
│ │ ├── ComplianceOverrideRequest.java
│ │ ├── CreateTaxConfigRequest.java
│ │ ├── UpdateTaxConfigRequest.java
│ │ ├── CreateTerminalRequest.java
│ │ ├── UpdateTerminalRequest.java
│ │ ├── UpdateStoreSettingsRequest.java
│ │ ├── UpdateTaxRateRequest.java
│ │ ├── UpdateTimezoneRequest.java
│ │ ├── UpdateOperatingHoursRequest.java
│ │ ├── UpdateOnboardingPhaseRequest.java
│ │ ├── CreateCustomerRequest.java
│ │ ├── UpdateCustomerRequest.java
│ │ ├── AdjustPointsRequest.java
│ │ ├── CreatePromotionRequest.java
│ │ ├── UpdatePromotionRequest.java
│ │ ├── CreateNotificationRequest.java
│ │ ├── CreateStockTransferRequest.java
│ │ ├── ReceiveStockTransferRequest.java
│ │ ├── QueryRequest.java
│ │ └── SkanDataRequest.java
│ └── response/ [Phase 1+]
│ ├── AuthResponse.java
│ ├── MeResponse.java
│ ├── InviteResponse.java
│ ├── OrgMemberResponse.java
│ ├── StoreMemberResponse.java
│ ├── ProductResponse.java
│ ├── CategoryResponse.java
│ ├── CategoryTreeResponse.java
│ ├── SupplierResponse.java
│ ├── ProductSupplierResponse.java
│ ├── InventoryResponse.java
│ ├── InventoryHistoryResponse.java
│ ├── SerialNumberResponse.java
│ ├── TransactionResponse.java
│ ├── TransactionDetailResponse.java
│ ├── TransactionSummaryResponse.java
│ ├── DailySalesResponse.java
│ ├── ReturnResponse.java
│ ├── ReturnWithDetailsResponse.java
│ ├── DiscountResponse.java
│ ├── ShiftPreferenceResponse.java
│ ├── ShiftAssignedResponse.java
│ ├── ShiftStatusResponse.java
│ ├── ShiftRequestResponse.java
│ ├── ComplianceCheckResponse.java
│ ├── ComplianceSummaryResponse.java
│ ├── TaxConfigResponse.java
│ ├── TerminalResponse.java
│ ├── StoreSettingsResponse.java
│ ├── OnboardingStatusResponse.java
│ ├── CustomerResponse.java
│ ├── PromotionResponse.java
│ ├── NotificationResponse.java
│ ├── StockTransferResponse.java
│ ├── ActivityLogResponse.java
│ ├── DashboardReportResponse.java
│ ├── SalesReportResponse.java
│ ├── InventoryReportResponse.java
│ ├── MembersReportResponse.java
│ ├── QueryResponse.java
│ ├── QuerySchemaResponse.java
│ └── SkanDataResponse.java
│
├── mapper/
│ │ (Static utility classes. Map SQLDelight-generated Kotlin types
│ │ to response DTOs, and request DTOs to query parameters.)
│ ├── UserMapper.java [Phase 1]
│ ├── InviteMapper.java [Phase 1]
│ ├── OrgMemberMapper.java [Phase 2]
│ ├── StoreMemberMapper.java [Phase 2]
│ ├── ProductMapper.java [Phase 3]
│ ├── CategoryMapper.java [Phase 3]
│ ├── SupplierMapper.java [Phase 3]
│ ├── InventoryMapper.java [Phase 4]
│ ├── SerialNumberMapper.java [Phase 4]
│ ├── TransactionMapper.java [Phase 5]
│ ├── ReturnMapper.java [Phase 5]
│ ├── DiscountMapper.java [Phase 5]
│ ├── ShiftMapper.java [Phase 6]
│ ├── ComplianceMapper.java [Phase 6]
│ ├── TaxConfigMapper.java [Phase 6]
│ ├── TerminalMapper.java [Phase 6]
│ ├── CustomerMapper.java [Phase 7]
│ ├── PromotionMapper.java [Phase 7]
│ ├── NotificationMapper.java [Phase 7]
│ ├── StockTransferMapper.java [Phase 7]
│ └── ActivityLogMapper.java [Phase 7]
│
└── exception/
├── GlobalExceptionHandler.java [EXISTS]
├── ResourceNotFoundException.java [EXISTS]
├── UnauthorizedException.java [EXISTS]
├── ForbiddenException.java [EXISTS]
├── ConflictException.java [Phase 1]
└── BadRequestException.java [Phase 1]
File counts by phase:
| Phase | Controllers | Services | Repositories | DTOs (Req+Res) | Mappers | Other | Total |
|---|---|---|---|---|---|---|---|
| 0a | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
| 0b | 0 | 2 | 0 | 0 | 0 | 8 | 10 |
| 1 | 4 | 3 | 2 | 6+3 | 2 | 2 | 22 |
| 2 | 2 | 2 | 4 | 2+2 | 2 | 0 | 14 |
| 3 | 3 | 3 | 4 | 5+4 | 3 | 0 | 22 |
| 4 | 2 | 2 | 3 | 3+3 | 2 | 0 | 15 |
| 5 | 3 | 3 | 3 | 3+7 | 3 | 0 | 22 |
| 6 | 6 | 6 | 4 | 11+9 | 4 | 0 | 40 |
| 7 | 5 | 5 | 5 | 6+5 | 5 | 0 | 31 |
| 8 | 3 | 3 | 0 | 2+6 | 0 | 0 | 14 |
| 9 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
| Sum | 28 | 29 | 25 | 80 | 21 | 12 | 192 |
1.5 Entity Relationship Summary
Organization ─────────────────────────────────────────────────────────┐
│ │
├── OrgMembership ──── User ──── StoreMembership ──── Store ──────────┤
│ │ │ │
├── Invite (org-level) │ ├── Invite │
├── ProductCategory │ │ (store) │
├── Supplier ───────────┼── ProductSupplier ─── Product─┤ │
│ │ │ ├── Terminal │
├── Customer │ │ ├── TaxConfig │
│ │ │ │ │
├── Notification │ ┌────────┘ │ │
├── Promotion ──────────┼──────────────┤ │ │
│ │ │ │ │
├── StockTransfer ──────┼──── StockTransferItem │ │
│ (from/to stores) │ │ │
│ │ InventoryEntry ──────┤ │
│ │ │ │ │
│ │ InventoryLevel │ │
│ │ │ │ │
│ │ LedgerEntry │ │
│ │ │ │
│ │ SerialNumber ────────┤ │
│ │ │ │
│ │ Transaction ─────────┤ │
│ │ ├── TransactionItem │ │
│ │ ├── TransactionPayment │
│ │ └── Return │ │
│ │ │ │
│ │ Discount ────────────┤ │
│ │ │ │
│ │ ShiftPreference │ │
│ │ ShiftAssigned ───────┤ │
│ │ ShiftActual │ │
│ │ ShiftRequest │ │
│ │ │ │
│ │ ComplianceCheck ─────┤ │
│ │ │ │
├── ActivityLog ────────┘ │ │
│ │ │
└──────────────────────────────────────────────────────┘──────────────┘
Legend:
─── = foreign key relationship
Organization owns: Suppliers, Categories, Customers, Notifications,
Promotions, StockTransfers, SerialNumbers
Store owns: Products, Inventory, Transactions, Discounts,
Returns, Shifts, Compliance, TaxConfigs, Terminals
User participates: Memberships, Shifts, Transactions, ComplianceChecks
Key dependency chains (determines build phase order):
Organization→Store→Product→InventoryEntry→LedgerEntryOrganization→User→OrgMembership/StoreMembershipStore→Transaction→TransactionItem/TransactionPaymentStore→ShiftAssigned→ShiftActualProduct→SerialNumber(tracks individual units)Organization→StockTransfer→StockTransferItem(references products + stores)