npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@classytic/pos

v0.2.1

Published

Multi-vertical POS / register-session kernel for MongoDB — shift lifecycle, cash-drawer reconciliation, payment-method breakdown, variance detection, FSM. Works for retail, restaurant, clinic, service-booking, car-wash and any vertical with a 'shift opens

Readme

@classytic/pos

Multi-vertical POS / register-session kernel for MongoDB. Shift lifecycle, cash-drawer reconciliation, payment-method breakdown, variance detection, FSM. Works for retail, restaurant, clinic, service-booking, car-wash and any vertical with a "shift opens, shift closes, money is reconciled" workflow.

Built on @classytic/mongokit and @classytic/primitives. Plugs into @classytic/arc for HTTP — but works standalone with any framework.

Why

Date-based aggregation ("close the day") is a pre-2010 retail pattern that hides per-shift accountability. Industry standard (Odoo, Square, Shopify POS, Lightspeed, Microsoft Dynamics 365 Retail, ARTS) is shift-driven: a register opens, accumulates orders, closes with a count, posts a single journal entry. Multi-cashier same-day = parallel shifts, summed by the ledger.

This package is that primitive. It has no opinion about your chart of accounts, payment methods, or accounting framework — those plug in via bridges.

Verticals

| Vertical | Maps to | |---|---| | Retail (grocery, boutique, hypermarket) | One shift per register per cashier-day | | Restaurant | One shift per server section, or per register; kitchen routing in extra fields | | Clinic / doctor booking | One shift per front desk per day; copays + appointment fees | | Service booking (salon, spa) | One shift per chair / station | | Car wash | One shift per bay |

The package's only assumption: "a session opens, accumulates payments, reconciles cash, closes." How you slice that across cashiers / registers / stations is your host's choice.

Install

npm install @classytic/pos

Peer deps: mongoose >=9.4.1, @classytic/mongokit >=3.16.0, @classytic/primitives >=0.7.2, @classytic/repo-core >=0.6.0, zod >=4.0.0.

Transactions required. The close pipeline is CAS-driven and hosts routinely wrap the shift + outbox writes in a transaction, so createPosEngine asserts the backend declares the transactions capability at boot (MongoDB replica set). Pass allowNonTransactional: true for standalone-Mongo local dev.

Quick start

import mongoose from 'mongoose';
import { createPosEngine } from '@classytic/pos';

const pos = createPosEngine({
  connection: mongoose.connection,
  defaultPolicy: {
    requiredOpeningFloat: null,
    blindCloseRequired: false,
    varianceThresholdAbs: 100,    // BDT 100
    varianceThresholdPct: 0.5,    // 0.5% of expected
    managerOverrideRequired: true,
    allowHandover: true,
    requireReasonCode: true,
    allowedReasonCodes: ['safe_drop', 'petty_cash', 'bank_deposit', 'till_top_up', 'other'],
    allowedPaymentMethods: ['cash', 'card', 'mfs', 'bank_transfer'],
  },
  bridges: {
    ledger: {
      async onShiftClosed(shift, ctx) {
        // Build journal entry from shift.paymentBreakdown, post via your ledger
        // engine, return { journalEntryId }.
        const je = await myLedger.post(buildShiftJE(shift));
        return { journalEntryId: je._id.toString() };
      },
    },
  },
});

const ctx = { organizationId: 'branch-1', actorId: 'user-1' };

// Open a shift
const shift = await pos.repositories.shift.open({
  registerId: 'register-1',
  businessDate: new Date(),
  openingCashierId: 'cashier-1',
  openingCashierName: 'Alice',
  openingCash: 1000,
}, ctx);

// As orders post in your host, increment the breakdown:
await pos.repositories.shift.incrementSales({
  shiftId: shift._id.toString(),
  method: 'cash',
  amount: 250,
}, ctx);

// At end of shift — variance check + ledger bridge + state write
await pos.repositories.shift.close({
  shiftId: shift._id.toString(),
  countedByMethod: { cash: 1245 },
  closedBy: 'cashier',
}, ctx);

State machine

open ─► paused ─► open ─► blind_closed ─► closed
 │                 │            │
 └─► forceClose ◄──┴────────────┴────► orphaned_closed

| State | Meaning | |---|---| | open | Accepting sales; drawer live | | paused | Toast-style handover; drawer rejects new sales until resume | | blind_closed | Lightspeed pattern; cashier counted, awaits manager reconcile | | closed | Reconciled, immutable | | orphaned_closed | EOD cron auto-closed; counts default to expected; flagged for review |

Bridges

All optional. The package degrades gracefully without any.

  • ledgeronShiftClosed(shift, ctx) → { journalEntryId }. Host emits the JE. Throw to roll back the close.
  • policyresolvePolicy(ctx) → ShiftPolicy. Per-branch policy override at open time. Without it, defaultPolicy is used.
  • notificationonVarianceOverride, onOrphanedClose. Best-effort; failures logged not raised.

Arc 2.11 wiring

import { defineResource, createMongooseAdapter } from '@classytic/arc';
import { openShiftBody, blindCloseBody, closeBody } from '@classytic/pos/schemas';

export default defineResource({
  name: 'shift',
  prefix: '/pos/shifts',
  adapter: createMongooseAdapter(pos.models.Shift, pos.repositories.shift),
  customSchemas: {
    create: { body: openShiftBody },
  },
  actions: {
    pause:        { handler: (id, _, req) => pos.repositories.shift.pause(id, ctxFrom(req)) },
    resume:       { handler: (id, _, req) => pos.repositories.shift.resume(id, ctxFrom(req)) },
    blind_close:  { handler: (id, data, req) => pos.repositories.shift.blindClose({ shiftId: id, ...data }, ctxFrom(req)), schema: blindCloseBody },
    close:        { handler: (id, data, req) => pos.repositories.shift.close({ shiftId: id, ...data }, ctxFrom(req)), schema: closeBody },
    force_close:  { handler: (id, _, req) => pos.repositories.shift.forceClose(id, ctxFrom(req)) },
  },
});

Auto-CRUD (list/get/create/update/delete) comes from arc + mongokit. State transitions go through declarative actions. Custom routes only when you genuinely need them.

Events & transactional outbox

Every domain verb emits a pos:* event. Inject any arc EventTransport (Memory / Redis / Kafka) — the package falls back to an in-process bus.

For at-least-once delivery, wire a host-owned OutboxStore. Each event is then saved to the outbox under ctx.session before the transport publish, so the event row commits atomically with the shift write. Save failures propagate (the host's transaction rolls back); publish failures are swallowed (the relay re-delivers from the durable row).

import { createPosEngine } from '@classytic/pos';
import { MongoOutboxStore } from '@classytic/arc'; // or any OutboxStore impl

const pos = createPosEngine({
  connection,
  defaultPolicy,
  eventTransport: redisTransport,
  outbox: new MongoOutboxStore({ connection, name: 'outbox' }),
});

Hosts wanting publish-time validation / OpenAPI introspection register the Zod event catalog with arc's EventRegistry:

import { posEventDefinitions } from '@classytic/pos/events';
for (const def of posEventDefinitions) registry.register(def);

Subpath exports

import { createPosEngine } from '@classytic/pos';                 // engine + types
import { ShiftRepository, type IShift } from '@classytic/pos';     // repo + model types
import {
  POS_EVENTS, InProcessPosBus,
  posEventDefinitions, MemoryOutboxStore, type OutboxStore,
} from '@classytic/pos/events';
import { canTransition, ShiftPolicy } from '@classytic/pos/domain';
import { openShiftBody, closeBody } from '@classytic/pos/schemas';
import { makePolicy, makeOpenShiftInput } from '@classytic/pos/testing';

License

MIT — see LICENSE.