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