Skip to main content

POS Service -- Getting Started

This guide gets you connected to the POS service and making your first API calls in 15 minutes.


What is POS?

The POS service is the source of truth for all point-of-sale transactional operations. It manages catalog (products, variants, modifiers, pricing, tax rules), sales (orders, payments), inventory (stock, locations, purchase orders), customers (profiles, loyalty, points), staff (shifts, drawers, commissions), promotions (coupons, discounts), and payment gateway integration (Midtrans).

POS does not handle authentication (the Auth service does) or identity/permissions (the IAM service does).


Connection Setup

Import the generated stubs

Add the POS module as a dependency:

go get github.com/TopengDev/aenoxa_pos@latest

Create a gRPC connection

package main

import (
"log"

posv1 "github.com/TopengDev/aenoxa_pos/gen/pos/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)

func main() {
conn, err := grpc.NewClient("localhost:50053",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
defer conn.Close()

catalogClient := posv1.NewCatalogServiceClient(conn)
salesClient := posv1.NewSalesServiceClient(conn)
inventoryClient := posv1.NewInventoryServiceClient(conn)
customerClient := posv1.NewCustomerServiceClient(conn)
staffClient := posv1.NewStaffServiceClient(conn)
promotionClient := posv1.NewPromotionServiceClient(conn)
paymentClient := posv1.NewPaymentTransactionServiceClient(conn)

// Use the clients...
_ = catalogClient
_ = salesClient
_ = inventoryClient
_ = customerClient
_ = staffClient
_ = promotionClient
_ = paymentClient
}

Authentication

POS supports three authentication modes:

Development mode (default)

When POS_AUTH_ENABLED=false, authentication is bypassed. Use dev headers to simulate tenant context:

import "google.golang.org/grpc/metadata"

ctx := metadata.AppendToOutgoingContext(ctx,
"x-dev-tenant-id", "00000000-0000-0000-0000-000000000001",
"x-dev-user-id", "00000000-0000-0000-0000-000000000001",
"x-dev-membership-id", "00000000-0000-0000-0000-000000000001",
)

JWT authentication

When POS_AUTH_ENABLED=true, attach a tenant-scoped JWT from the Auth service:

ctx := metadata.AppendToOutgoingContext(ctx,
"authorization", "Bearer "+accessToken,
)

The JWT must be an Ed25519-signed tenant-scoped token with tenant_id, membership_id, and scope claims. POS validates the signature by fetching JWKS from the Auth service.

API key authentication

For service-to-service calls:

ctx := metadata.AppendToOutgoingContext(ctx,
"x-api-key", "your-api-key",
)

Quick Start: Create a Product and Place an Order

1. Health check

grpcurl -plaintext localhost:50053 pos.v1.HealthService/Check

2. Create a category

grpcurl -plaintext \
-H "X-Dev-Tenant-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-Membership-Id: 00000000-0000-0000-0000-000000000001" \
-d '{"name": "Beverages", "sort_order": 1}' \
localhost:50053 pos.v1.CatalogService/CreateCategory

3. Create a product

grpcurl -plaintext \
-H "X-Dev-Tenant-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-Membership-Id: 00000000-0000-0000-0000-000000000001" \
-d '{"name": "Iced Latte", "sku": "ICE-LATTE-001", "category_id": "<category_id>"}' \
localhost:50053 pos.v1.CatalogService/CreateProduct

4. Create a variant and set a price

# Create variant
grpcurl -plaintext \
-H "X-Dev-Tenant-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-Membership-Id: 00000000-0000-0000-0000-000000000001" \
-d '{"product_id": "<product_id>", "name": "Regular", "sku": "ICE-LATTE-001-REG"}' \
localhost:50053 pos.v1.CatalogService/CreateVariant

# Set price (amount in smallest currency unit, e.g., 2500000 = Rp 25,000)
grpcurl -plaintext \
-H "X-Dev-Tenant-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-Membership-Id: 00000000-0000-0000-0000-000000000001" \
-d '{"variant_id": "<variant_id>", "amount": "2500000", "currency": "IDR"}' \
localhost:50053 pos.v1.CatalogService/SetPrice

5. Create an order, add items, and finalize

# Create a draft order
grpcurl -plaintext \
-H "X-Dev-Tenant-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-Membership-Id: 00000000-0000-0000-0000-000000000001" \
-d '{}' \
localhost:50053 pos.v1.SalesService/CreateOrder

# Add a line item
grpcurl -plaintext \
-H "X-Dev-Tenant-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-Membership-Id: 00000000-0000-0000-0000-000000000001" \
-d '{"order_id": "<order_id>", "product_id": "<product_id>", "variant_id": "<variant_id>", "quantity": 2}' \
localhost:50053 pos.v1.SalesService/AddOrderItem

# Finalize the order (snapshots prices, calculates taxes, transitions to OPEN)
grpcurl -plaintext \
-H "X-Dev-Tenant-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-Membership-Id: 00000000-0000-0000-0000-000000000001" \
-d '{"order_id": "<order_id>"}' \
localhost:50053 pos.v1.SalesService/FinalizeOrder

6. Add payment and complete

# Add a cash payment
grpcurl -plaintext \
-H "X-Dev-Tenant-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-Membership-Id: 00000000-0000-0000-0000-000000000001" \
-d '{"order_id": "<order_id>", "payment_type": "CASH", "amount": "5000000"}' \
localhost:50053 pos.v1.SalesService/AddPayment

# Complete the order
grpcurl -plaintext \
-H "X-Dev-Tenant-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-Membership-Id: 00000000-0000-0000-0000-000000000001" \
-d '{"order_id": "<order_id>"}' \
localhost:50053 pos.v1.SalesService/CompleteOrder

Pagination

List endpoints accept an optional PaginationRequest and return a PaginationResponse.

FieldTypeDescription
page_sizeint32Number of items to return. Default: 50. Max: 100.
page_tokenstringOpaque cursor (UUID of last item from previous page). Omit for first page.
FieldTypeDescription
next_page_tokenstringToken for the next page. Empty when no more results.
total_countint32Number of items returned in this page.

Error Codes

gRPC CodeDomain CauseDescriptionRetry?
NOT_FOUNDResource does not existThe requested entity was not found by ID.No
ALREADY_EXISTSDuplicate resource / unique constraintA resource with the given natural key already exists.No
INVALID_ARGUMENTValidation failure, bad UUIDThe request contains an invalid or missing field.No (fix request)
FAILED_PRECONDITIONInvalid state transitionThe operation cannot be performed in the current state (e.g., finalizing a non-DRAFT order).No (resolve precondition)
RESOURCE_EXHAUSTEDRate limit exceededThe per-IP rate limiter rejected the request.Yes (backoff)
UNAUTHENTICATEDMissing or invalid credentialsNo valid JWT or API key was provided.No (fix credentials)
INTERNALUnexpected server errorAn unhandled error occurred.Yes (with backoff)

Key Concepts

Multi-tenancy

Every request requires a tenant_id (from JWT claims or dev headers). All data is strictly tenant-scoped. Cross-tenant data access is impossible by design.

Money as Integer

All monetary values are stored as int64 in the smallest currency unit (e.g., cents for USD, rupiah for IDR). The field amount: 2500000 with currency: "IDR" represents Rp 25,000. This eliminates floating-point rounding errors.

Order Lifecycle

DRAFT  ──(FinalizeOrder)──>  OPEN  ──(CompleteOrder)──>  COMPLETED
│ │
└──(VoidOrder)──> VOIDED <─┘
  • DRAFT: Items can be added, removed, and modified. No prices are locked.
  • OPEN: Prices, taxes, and modifiers are snapshotted. Payments can be added.
  • COMPLETED: Fully paid order. Terminal state.
  • VOIDED: Cancelled order (requires reason). Terminal state.

Idempotency

All write operations are safe to retry. POS uses DB constraints for natural idempotency and idempotency keys for request-level deduplication. Payment charges accept an idempotency_key to prevent duplicate charges.