Your First Operation

Build a payment capture operation with policy enforcement in under 10 minutes.

What you'll build

A real-world payment processing operation that validates input, enforces business rules, integrates with Stripe, and audits every transaction. This is production-ready code, not a toy example.

1. Define the operation spec

Create lib/specs/billing/capture-payment.ts:

import { defineCommand } from '@lssm/lib.contracts';
import { SchemaModel, ScalarTypeEnum } from '@lssm/lib.schema';

const CapturePaymentInput = new SchemaModel({
  name: 'CapturePaymentInput',
  fields: {
    invoiceId: { type: ScalarTypeEnum.NonEmptyString(), isOptional: false },
    amount: { type: ScalarTypeEnum.PositiveNumber(), isOptional: false },
    currency: { type: ScalarTypeEnum.String(), isOptional: false },
    paymentMethodId: { type: ScalarTypeEnum.NonEmptyString(), isOptional: false },
  },
});

const PaymentResult = new SchemaModel({
  name: 'PaymentResult',
  fields: {
    transactionId: { type: ScalarTypeEnum.String(), isOptional: false },
    status: { type: ScalarTypeEnum.String(), isOptional: false },
    receiptUrl: { type: ScalarTypeEnum.String(), isOptional: true },
  },
});

export const CapturePayment = defineCommand({
  meta: {
    name: 'billing.capturePayment',
    version: 1,
    description: 'Process a payment for an invoice',
    goal: 'Capture funds from customer payment method',
    context: 'Called when customer confirms purchase',
    owners: ['team-billing'],
    tags: ['payments', 'stripe', 'critical'],
    stability: 'stable',
  },
  io: {
    input: CapturePaymentInput,
    output: PaymentResult,
  },
  policy: {
    auth: 'user',
    rules: [
      { resource: 'invoice', action: 'pay', condition: 'owner' },
    ],
  },
});

2. Implement the handler

Create lib/handlers/billing/capture-payment.ts:

import { CapturePayment } from '@/lib/specs/billing/capture-payment';
import { stripe } from '@/lib/integrations/stripe';
import { db } from '@/lib/db';

export async function handleCapturePayment(input, ctx) {
  // 1. Verify invoice exists and belongs to user
  const invoice = await db.invoice.findUnique({
    where: { id: input.invoiceId, userId: ctx.userId },
  });
  
  if (!invoice) throw new Error('Invoice not found');
  if (invoice.status === 'paid') throw new Error('Already paid');

  // 2. Create Stripe payment intent
  const paymentIntent = await stripe.paymentIntents.create({
    amount: Math.round(input.amount * 100),
    currency: input.currency,
    payment_method: input.paymentMethodId,
    confirm: true,
    metadata: { invoiceId: input.invoiceId },
  });

  // 3. Update invoice status
  await db.invoice.update({
    where: { id: input.invoiceId },
    data: { 
      status: 'paid', 
      paidAt: new Date(),
      transactionId: paymentIntent.id,
    },
  });

  return {
    transactionId: paymentIntent.id,
    status: paymentIntent.status,
    receiptUrl: paymentIntent.charges.data[0]?.receipt_url,
  };
}

3. Register and serve

Wire it up in lib/registry.ts and app/api/ops/[...route]/route.ts:

// lib/registry.ts
import { SpecRegistry, installOp } from '@lssm/lib.contracts';
import { CapturePayment } from './specs/billing/capture-payment';
import { handleCapturePayment } from './handlers/billing/capture-payment';

export const registry = new SpecRegistry();
installOp(registry, CapturePayment, handleCapturePayment);

// app/api/ops/[...route]/route.ts
import { makeNextAppHandler } from '@lssm/lib.contracts/server/rest-next-app';
import { registry } from '@/lib/registry';
import { auth } from '@/lib/auth';

const handler = makeNextAppHandler(registry, async (req) => {
  const session = await auth(req);
  return { actor: 'user', userId: session.userId, tenantId: session.tenantId };
});

export { handler as GET, handler as POST };

What you just built:

  • ✓ Type-safe API endpoint at /api/ops/billing.capturePayment
  • ✓ Automatic input validation (amount must be positive, IDs required)
  • ✓ Policy enforcement—only invoice owner can pay
  • ✓ Stripe integration with error handling
  • ✓ Database transaction with audit trail
  • ✓ Same spec works with GraphQL, MCP, or webhooks