Skip to main content

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 login
  • GET /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

ScopeCount
Public (no auth)5
Authenticated (JWT required)2
Organization-scoped55
Store-scoped93
Total155

0.3 Tech Stack

LayerTechnology
LanguageJava 25 (--release 25), no Lombok
FrameworkSpring Boot 4.0.0 / Spring Framework 7.0 (HTTP/REST, validation, security)
DatabaseGoogle Cloud Spanner (PostgreSQL interface)
Local DBPostgreSQL 15+ (localhost:5432/pinpoint)
Schema & Data LayerSQLDelight — .sq files generate shared Kotlin types
Shared Typeskt_jvm_library consumed by Android, terminal-api, merchant-api via Kotlin-Java bytecode interop
AuthJJWT 0.12.5, BCrypt (Spring Security)
BuildBazel 9.0.0 (bzlmod), Maven deps under @maven_spring//
ContainerOCI image, distroless Java 25 base, port 8080
IAMAllow-all stub (Dafny spec at specs/playground/iam.dfy not finalized)

0.4 Existing Boilerplate (9 files)

FileStatus
CoreApplication.javaKeep — Spring Boot entry point
config/JpaConfig.javaRemove — replaced by SQLDelight DatabaseConfig
controller/HealthController.javaKeepGET /health
dto/ApiResponse.javaKeep — response envelope record
entity/BaseEntity.javaRemove — replaced by SQLDelight-generated types
exception/GlobalExceptionHandler.javaKeep — centralized error handling
exception/ResourceNotFoundException.javaKeep
exception/UnauthorizedException.javaKeep
exception/ForbiddenException.javaKeep

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 ComponentIDJava/Kotlin Mapping
JWT Validator & User Locator2qFJqSyc7PcVuv3SzjqUsecurity.filter.JwtAuthenticationFilter, security.service.JwtService, security.service.UserLocatorService
End-User Security Moduleoh3tdWdLe307VoL8QU8ysecurity.filter.OrgContextFilter, security.filter.StoreContextFilter, security.context.OrgContext, security.context.StoreContext
Formal Verified IAM ModulegU2rWxgSFybR0v4XKhGdiam.IamService (allow-all stub), iam.IamDecisionEngine (Phase 9)
Auth WrappernWvdC16eDje1WXE3Nd4Msecurity.service.AuthWrapperService (BCrypt hashing, token generation)
Authentication API RoutesgDjNSTJnNH1pA0opsvnpcontroller.auth.AuthController, controller.auth.PinAuthController
Organization API RoutesqTu7Ci4UU3DLEyin3Me0controller.org.* (all org-scoped controllers)
Store API Routes4LyRc699nG863bDes5jbcontroller.store.* (all store-scoped controllers)
User REST/CRUD interfaceaElkzEYQoZLWZIHq6uVRservice.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:

  1. Load balancer routes to merchant-api
  2. JwtAuthenticationFilter extracts Authorization: Bearer <token>, validates via JJWT, resolves user from DB, sets SecurityContext
  3. OrgContextFilter extracts :org_id from path, validates user's org membership, sets OrgContext
  4. StoreContextFilter extracts :store_id from path, validates user's store membership, sets StoreContext
  5. Controller method executes with authenticated user + org/store context available
  6. IamService.checkAccess() called (currently returns true — 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 — .sq files 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:

PhaseControllersServicesRepositoriesDTOs (Req+Res)MappersOtherTotal
0a0000011
0b02000810
14326+32222
22242+22014
33345+43022
42233+32015
53333+73022
666411+94040
75556+55031
83302+60014
90000011
Sum282925802112192

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

  1. OrganizationStoreProductInventoryEntryLedgerEntry
  2. OrganizationUserOrgMembership / StoreMembership
  3. StoreTransactionTransactionItem / TransactionPayment
  4. StoreShiftAssignedShiftActual
  5. ProductSerialNumber (tracks individual units)
  6. OrganizationStockTransferStockTransferItem (references products + stores)