> Documentation index: https://stilldeeppos.com/llms.txt

# StillDeep SDK — TypeScript / Node.js client

## Prerequisites

- Node.js 22 LTS+
- `protoc` 25+
- `ts-proto` plugin (`npm i -D ts-proto`)

## Setup

```bash
mkdir my-pos-client && cd my-pos-client
npm init -y
npm i @grpc/grpc-js @grpc/proto-loader
npm i -D typescript ts-proto @types/node
cp -r ../waxpos/src/public/StillDeep.Intelligence.Contracts/protos ./protos

mkdir -p src/generated

protoc \
  --plugin=./node_modules/.bin/protoc-gen-ts_proto \
  --ts_proto_out=./src/generated \
  --ts_proto_opt=outputServices=grpc-js,esModuleInterop=true \
  --proto_path=./protos \
  protos/common.proto protos/receipt.proto protos/sales.proto protos/catalog.proto \
  protos/member.proto protos/device.proto protos/events.proto \
  protos/user.proto protos/shift.proto
```

ts-proto maps fields to camelCase (`cart_id` -> `cartId`), `int64` to `number`, and generates `fromPartial()` to build requests.

## Example: sign in, open the shift, sell one item

Sequence and required fields: see [`getting-started.md`](https://stilldeeppos.com/docs/reference/sdk.md). Run as an ES module (top-level `await`).

```ts
import { credentials, Metadata, ServiceError, status } from '@grpc/grpc-js';
import { randomUUID } from 'node:crypto';
import { AuthMode, AuthenticateRequest, AuthenticateResponse, UserServiceClient } from './generated/user';
import { GetCurrentShiftRequest, OpenShiftRequest, OpenShiftResponse, Shift, ShiftServiceClient } from './generated/shift';
import {
  AddItemRequest, BeginPaymentRequest, CartResponse, CreateCartRequest, FinalizeRequest,
  PaymentResponse, PaymentStatus, ProcessPaymentRequest, SalesServiceClient, TransactionResponse,
} from './generated/sales';

const address = process.env.STILLDEEP_GRPC ?? 'localhost:50051';
const postId = process.env.STILLDEEP_POST_ID!;          // required: id of this post
const sku = process.env.STILLDEEP_SKU!;                 // product to sell
const methodCode = process.env.STILLDEEP_METHOD ?? 'cash'; // a code from ConfigService.GetPostConfig

const creds = credentials.createInsecure();
const users = new UserServiceClient(address, creds);
const shifts = new ShiftServiceClient(address, creds);
const sales = new SalesServiceClient(address, creds);

const unary = <T>(fn: (cb: (err: ServiceError | null, res: T) => void) => void) =>
  new Promise<T>((resolve, reject) => fn((err, res) => (err ? reject(err) : resolve(res))));

// 1. Authenticate (required).
const auth = await unary<AuthenticateResponse>((cb) => users.authenticate(AuthenticateRequest.fromPartial({
  postId, mode: AuthMode.AUTH_MODE_PASSWORD, login: process.env.STILLDEEP_LOGIN!, password: process.env.STILLDEEP_PASSWORD!,
}), cb));
const userId = auth.user!.userId;

// 2-3. Reuse the open shift, or open one.
let shift: Shift;
try {
  shift = await unary<Shift>((cb) => shifts.getCurrentShift(GetCurrentShiftRequest.fromPartial({ postId }), cb));
} catch (e) {
  if ((e as ServiceError).code !== status.NOT_FOUND) throw e;
  const opened = await unary<OpenShiftResponse>((cb) => shifts.openShift(OpenShiftRequest.fromPartial({
    postId, userId, openingCash: { cents: 0 }, idempotencyKey: randomUUID(),
  }), cb));
  shift = opened.shift!;
}

// Every following call carries x-shift-id.
const md = new Metadata();
md.set('x-shift-id', shift.shiftId);

// 4. CreateCart (required).
const created = await unary<CartResponse>((cb) => sales.createCart(CreateCartRequest.fromPartial({
  postId, idempotencyKey: randomUUID(),
}), md, cb));
const cartId = created.cart!.cartId;

// 5. AddItem (required): product_id, sku or barcode; quantity in milli-units.
await unary<CartResponse>((cb) => sales.addItem(AddItemRequest.fromPartial({
  cartId, sku, quantity: { milliUnits: 1000 }, idempotencyKey: randomUUID(),
}), md, cb));

// 6. BeginPayment (optional): handle an extension question before paying.
const checkout = await unary<CartResponse>((cb) => sales.beginPayment(BeginPaymentRequest.fromPartial({ cartId }), md, cb));
if (checkout.question) throw new Error(`Answer required: ${checkout.question.code}`);

// 8. ProcessPayment (required): pay the remaining amount.
const paid = await unary<PaymentResponse>((cb) => sales.processPayment(ProcessPaymentRequest.fromPartial({
  cartId, methodCode, amount: checkout.cart!.remainingDue, idempotencyKey: randomUUID(),
}), md, cb));
if (paid.payment!.status === PaymentStatus.PAYMENT_STATUS_DECLINED
  || paid.payment!.status === PaymentStatus.PAYMENT_STATUS_FAILED) {
  throw new Error(paid.refusalGuide?.userFacingMessage ?? 'payment refused');
}

// 9. FinalizeTransaction (required).
const done = await unary<TransactionResponse>((cb) => sales.finalizeTransaction(FinalizeRequest.fromPartial({
  cartId, idempotencyKey: randomUUID(),
}), md, cb));
console.log('Receipt', done.transaction!.receiptNumber, 'change', paid.changeDue?.cents);
```

## Event stream

`EventService` is declared in `events.proto` but not served by the current `POSIntelligenceSvc` (`UNIMPLEMENTED`).

```ts
import { EventServiceClient, SubscribeRequest } from './generated/events';

const events = new EventServiceClient(address, creds);
const stream = events.subscribe(
  SubscribeRequest.fromPartial({ subscriberId: randomUUID(), kinds: [], postIdFilter: postId }), md);
stream.on('data', (ev) => console.log('event', ev));
stream.on('error', (e) => console.error(e));
```

## Known V1 limitations

- No TLS by default (local insecure channel). Over a network, add a certificate.
- No automatic client retry policy: the consumer implements it.
- `EventService.Heartbeat` (bidi streaming) is declared; prefer `Subscribe` for most cases.

Source: https://stilldeeppos.com/docs/reference/sdk/typescript
