# Quotation Send + Payment Flow Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Wire up quotation delivery (email with Stripe link, mock phone) and auto-PAID via Stripe webhook; remove Deals Pipeline + Pipeline Stats tabs; add Delivery Defaults to Settings.

**Architecture:** Expand `sendQuotation` in `quotations.service.ts` into a shared `dispatchQuotation` function that generates the portal token, creates a Stripe checkout session, and enqueues an email job. Both the create endpoint (when `sendImmediately=true`) and the send endpoint call this shared function. The Stripe webhook handler already calls `handleQuotationPaymentWebhook` — we just fix a metadata key mismatch bug.

**Tech Stack:** Next.js 14 App Router, Prisma 6, Stripe SDK, Redis job queue (`enqueueJob`), `fetchWithCSRF`, Zod validators, Jest

---

## Task 1: Remove Deals Pipeline + Pipeline Stats tabs

**Files:**

- Modify: `components/dashboard/quotations/QuotationsPanel.tsx`

No tests needed — this is pure deletion.

**Step 1: Remove imports, state, fetch logic, and tabs**

In `QuotationsPanel.tsx`, make these changes:

1. Remove imports: `DealsPipeline` and `PipelineStats`
2. Remove interfaces: `StageData` and `DealStats`
3. Remove translation keys: `deals` and `stats` from both `t.en` and `t.ar`
4. Change `TabKey` type: `type TabKey = "quotations" | "recurring" | "settings";`
5. Remove state: `pipeline`, `stats`, `pipelineLoading`
6. Remove the entire `fetchPipeline` callback
7. Remove the `useEffect` that calls `fetchPipeline`
8. Change `tabs` array to only include quotations, recurring, settings:

```tsx
const tabs: { key: TabKey; label: string }[] = [
  { key: "quotations", label: labels.quotations },
  { key: "recurring", label: labels.recurring },
  { key: "settings", label: labels.settings },
];
```

9. Remove the two JSX blocks for `deals` and `stats` tab content

**Step 2: Verify no TypeScript errors**

```bash
cd mawidi-site && npx tsc --noEmit 2>&1 | grep -i quotationspanel
```

Expected: no output (no errors)

**Step 3: Commit**

```bash
git add mawidi-site/components/dashboard/quotations/QuotationsPanel.tsx
git commit -m "feat(quotations): remove Deals Pipeline and Pipeline Stats tabs"
```

---

## Task 2: Add `sendImmediately` to create schema and form

**Files:**

- Modify: `lib/validators/quotations.validator.ts`
- Modify: `components/dashboard/quotations/QuotationForm.tsx`
- Test: `__tests__/lib/validators/quotations.validator.test.ts` (create if not exists)

**Step 1: Write failing test**

Create/open `mawidi-site/__tests__/lib/validators/quotations.validator.test.ts` and add:

```typescript
import { createQuotationSchema } from "@/lib/validators/quotations.validator";

describe("createQuotationSchema", () => {
  it("accepts sendImmediately: true", () => {
    const result = createQuotationSchema.safeParse({
      title: "Test",
      items: [],
      sendImmediately: true,
    });
    expect(result.success).toBe(true);
    expect(result.data?.sendImmediately).toBe(true);
  });

  it("defaults sendImmediately to true when omitted", () => {
    const result = createQuotationSchema.safeParse({
      title: "Test",
      items: [],
    });
    expect(result.success).toBe(true);
    expect(result.data?.sendImmediately).toBe(true);
  });

  it("accepts sendImmediately: false", () => {
    const result = createQuotationSchema.safeParse({
      title: "Test",
      items: [],
      sendImmediately: false,
    });
    expect(result.success).toBe(true);
    expect(result.data?.sendImmediately).toBe(false);
  });
});
```

**Step 2: Run test to verify it fails**

```bash
cd mawidi-site && npm test -- --testPathPattern="quotations.validator" --no-coverage 2>&1 | tail -20
```

Expected: FAIL — `sendImmediately` field not found in schema

**Step 3: Add field to schema**

In `lib/validators/quotations.validator.ts`, add to `createQuotationSchema`:

```typescript
export const createQuotationSchema = z.object({
  title: z.string().min(1).max(200),
  customerId: z.string().optional(),
  notes: z.string().max(2000).optional(),
  currency: z.string().length(3).default("QAR"),
  taxRate: z.number().min(0).max(100).default(0),
  discountRate: z.number().min(0).max(100).default(0),
  validUntil: z.string().datetime().optional(),
  items: z.array(quotationItemSchema).default([]),
  sendImmediately: z.boolean().default(true), // ← add this line
});
```

Also update the exported type:

```typescript
export type CreateQuotationInput = z.infer<typeof createQuotationSchema>;
// (no change needed — z.infer picks up the new field automatically)
```

**Step 4: Run test to verify it passes**

```bash
cd mawidi-site && npm test -- --testPathPattern="quotations.validator" --no-coverage 2>&1 | tail -10
```

Expected: PASS — 3 tests passing

**Step 5: Add checkbox to QuotationForm**

In `components/dashboard/quotations/QuotationForm.tsx`:

a) Add labels to both `t.en` and `t.ar`:

```typescript
// In t.en:
sendImmediately: "Send to customer immediately",

// In t.ar:
sendImmediately: "إرسال للعميل فوراً",
```

b) Add state (after `const [saving, setSaving] = useState(false);`):

```typescript
const [sendImmediately, setSendImmediately] = useState(true);
```

c) Add field to POST body in `handleSubmit` (add after `items: itemsPayload`):

```typescript
sendImmediately,
```

d) Add checkbox UI just above the `{/* Actions */}` div:

```tsx
{
  /* Send immediately (only for new quotations) */
}
{
  !quotation && (
    <div className="flex items-center gap-3 py-2">
      <input
        type="checkbox"
        id="sendImmediately"
        checked={sendImmediately}
        onChange={(e) => setSendImmediately(e.target.checked)}
        className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
      />
      <label htmlFor="sendImmediately" className="text-sm text-gray-700">
        {labels.sendImmediately}
      </label>
    </div>
  );
}
```

**Step 6: Verify TypeScript**

```bash
cd mawidi-site && npx tsc --noEmit 2>&1 | grep -i "quotationform\|sendimm" | head -10
```

Expected: no output

**Step 7: Commit**

```bash
git add mawidi-site/lib/validators/quotations.validator.ts \
        mawidi-site/components/dashboard/quotations/QuotationForm.tsx \
        mawidi-site/__tests__/lib/validators/quotations.validator.test.ts
git commit -m "feat(quotations): add sendImmediately field to schema and form checkbox"
```

---

## Task 3: Build `dispatchQuotation` service function

This is the core of the feature — generates portal token, creates Stripe checkout, enqueues email, mocks phone.

**Files:**

- Modify: `lib/services/quotations.service.ts`
- Test: `__tests__/lib/services/quotations-dispatch.test.ts` (new)

**Step 1: Write failing tests**

Create `mawidi-site/__tests__/lib/services/quotations-dispatch.test.ts`:

```typescript
// Mock all external dependencies before any imports
jest.mock("@/lib/db", () => ({
  prisma: { quotations: { findFirst: jest.fn(), update: jest.fn() } },
}));
jest.mock("@/lib/services/quotation-portal.service", () => ({
  createPortalCheckoutSession: jest.fn(),
}));
jest.mock("@/lib/queue/job-queue", () => ({
  enqueueJob: jest.fn().mockResolvedValue("job-1"),
}));
jest.mock("@paralleldrive/cuid2", () => ({
  createId: jest.fn().mockReturnValue("token-abc"),
}));

import { prisma } from "@/lib/db";
import * as portalService from "@/lib/services/quotation-portal.service";
import { enqueueJob } from "@/lib/queue/job-queue";
import { dispatchQuotation } from "@/lib/services/quotations.service";

const mockQuotation = {
  id: "q1",
  userId: "u1",
  quotationNumber: "Q-20260226-0001",
  title: "Test Quote",
  total: 500,
  currency: "QAR",
  portalToken: null,
  customer: {
    name: "Ahmed",
    email: "ahmed@example.com",
    phone: "+97450000001",
  },
  items: [{ description: "Service", quantity: 1, unitPrice: 500, total: 500 }],
  dueDate: null,
};

const mockUpdated = {
  ...mockQuotation,
  portalToken: "token-abc",
  status: "SENT",
  sentAt: new Date(),
};

beforeEach(() => {
  jest.clearAllMocks();
  (prisma.quotations.findFirst as jest.Mock).mockResolvedValue(mockQuotation);
  (prisma.quotations.update as jest.Mock).mockResolvedValue(mockUpdated);
  (portalService.createPortalCheckoutSession as jest.Mock).mockResolvedValue({
    success: true,
    checkoutUrl: "https://checkout.stripe.com/pay/cs_test_abc",
  });
});

describe("dispatchQuotation", () => {
  it("returns null when quotation not found", async () => {
    (prisma.quotations.findFirst as jest.Mock).mockResolvedValue(null);
    const result = await dispatchQuotation("u1", "q-missing");
    expect(result).toBeNull();
  });

  it("saves portal token and marks status SENT", async () => {
    await dispatchQuotation("u1", "q1");
    expect(prisma.quotations.update).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: "q1" },
        data: expect.objectContaining({
          portalToken: "token-abc",
          status: "SENT",
        }),
      }),
    );
  });

  it("reuses existing portal token if already set", async () => {
    (prisma.quotations.findFirst as jest.Mock).mockResolvedValue({
      ...mockQuotation,
      portalToken: "existing-token",
    });
    await dispatchQuotation("u1", "q1");
    expect(prisma.quotations.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({ portalToken: "existing-token" }),
      }),
    );
  });

  it("calls createPortalCheckoutSession with the portal token", async () => {
    await dispatchQuotation("u1", "q1");
    expect(portalService.createPortalCheckoutSession).toHaveBeenCalledWith(
      "token-abc",
      expect.stringContaining("/quotation/token-abc"),
      expect.any(String),
    );
  });

  it("enqueues an email job when customer has email", async () => {
    await dispatchQuotation("u1", "q1");
    expect(enqueueJob).toHaveBeenCalledWith(
      "email",
      expect.objectContaining({
        to: "ahmed@example.com",
        category: "quotation_sent",
      }),
      expect.anything(),
    );
  });

  it("returns phoneStatus: sent always", async () => {
    const result = await dispatchQuotation("u1", "q1");
    expect(result?.phoneStatus).toBe("sent");
  });

  it("returns emailQueued: false when customer has no email", async () => {
    (prisma.quotations.findFirst as jest.Mock).mockResolvedValue({
      ...mockQuotation,
      customer: { ...mockQuotation.customer, email: null },
    });
    const result = await dispatchQuotation("u1", "q1");
    expect(result?.emailQueued).toBe(false);
  });
});
```

**Step 2: Run test to verify it fails**

```bash
cd mawidi-site && npm test -- --testPathPattern="quotations-dispatch" --no-coverage 2>&1 | tail -20
```

Expected: FAIL — `dispatchQuotation is not exported from quotations.service`

**Step 3: Implement `dispatchQuotation`**

Add to `lib/services/quotations.service.ts` (after the existing imports, add the new imports):

```typescript
import { createId } from "@paralleldrive/cuid2";
import { createPortalCheckoutSession } from "@/lib/services/quotation-portal.service";
import { enqueueJob } from "@/lib/queue/job-queue";
import { createQuotationSentEmail } from "@/lib/services/email/templates/quotation-templates";
```

Then add the function (after `sendQuotation`):

```typescript
/**
 * Dispatch a quotation to the customer:
 * 1. Generate portal token (or reuse existing)
 * 2. Mark status = SENT
 * 3. Create Stripe checkout session via portal service
 * 4. Enqueue email with payment link
 * 5. Mock phone (log only)
 *
 * Returns null if quotation not found.
 */
export async function dispatchQuotation(
  userId: string,
  quotationId: string,
): Promise<{
  quotation: NonNullable<
    Awaited<ReturnType<typeof prisma.quotations.findFirst>>
  >;
  phoneStatus: "sent";
  emailQueued: boolean;
} | null> {
  const existing = await prisma.quotations.findFirst({
    where: { id: quotationId, userId },
    include: {
      customer: { select: { name: true, email: true, phone: true } },
      items: { orderBy: { sortOrder: "asc" } },
    },
  });
  if (!existing) return null;

  // Use existing portal token or generate a new one
  const portalToken = existing.portalToken ?? createId();

  // Update status and save portal token
  const quotation = await prisma.quotations.update({
    where: { id: quotationId },
    data: {
      status: "SENT",
      sentAt: new Date(),
      portalToken,
      updatedAt: new Date(),
    },
    include: {
      items: { orderBy: { sortOrder: "asc" } },
      customer: true,
    },
  });

  // Build portal URL
  const baseUrl = process.env.NEXTAUTH_URL ?? "http://localhost:9000";
  const portalUrl = `${baseUrl}/quotation/${portalToken}`;

  // Create Stripe checkout session (non-blocking on failure)
  try {
    await createPortalCheckoutSession(
      portalToken,
      `${portalUrl}?paid=1`,
      portalUrl,
    );
  } catch (err) {
    log.warn("Failed to create Stripe checkout session for quotation", {
      quotationId,
      err,
    });
  }

  // Enqueue email (non-blocking)
  let emailQueued = false;
  const customerEmail = existing.customer?.email;
  if (customerEmail) {
    try {
      const template = createQuotationSentEmail({
        quotationNumber: existing.quotationNumber,
        title: existing.title,
        customerName: existing.customer?.name ?? "Customer",
        total: existing.total,
        currency: existing.currency,
        dueDate: existing.dueDate?.toISOString(),
        portalUrl,
        items: existing.items.map((i) => ({
          description: i.description,
          quantity: i.quantity,
          unitPrice: i.unitPrice,
          total: i.total,
        })),
        lang: "en", // TODO: derive from customer locale when available
      });
      await enqueueJob(
        "email",
        {
          to: customerEmail,
          subject: template.subject,
          html: template.html,
          text: template.text,
          category: "quotation_sent",
          metadata: { quotationId, portalToken },
        },
        { priority: "high" },
      );
      emailQueued = true;
    } catch (err) {
      log.warn("Failed to enqueue quotation sent email", { quotationId, err });
    }
  }

  // Mock phone: log only
  log.info("SMS queued (mock)", {
    quotationId,
    phone: existing.customer?.phone ?? "unknown",
    message: `Quotation ${existing.quotationNumber} sent`,
  });

  return { quotation, phoneStatus: "sent", emailQueued };
}
```

**Step 4: Run tests to verify they pass**

```bash
cd mawidi-site && npm test -- --testPathPattern="quotations-dispatch" --no-coverage 2>&1 | tail -15
```

Expected: PASS — 7 tests passing

**Step 5: Commit**

```bash
git add mawidi-site/lib/services/quotations.service.ts \
        mawidi-site/__tests__/lib/services/quotations-dispatch.test.ts
git commit -m "feat(quotations): add dispatchQuotation — portal token, Stripe, email, mock SMS"
```

---

## Task 4: Wire `sendImmediately` into the create endpoint

**Files:**

- Modify: `app/api/dashboard/quotations/route.ts`

**Step 1: Write failing test**

Create `mawidi-site/__tests__/api/dashboard/quotations/create-dispatch.test.ts`:

```typescript
jest.mock("next-auth");
jest.mock("@/lib/auth", () => ({ auth: jest.fn() }));
jest.mock("@/lib/utils/auth-helpers.util", () => ({
  getAuthenticatedUser: jest
    .fn()
    .mockResolvedValue({ userId: "u1", email: "test@test.com" }),
}));
jest.mock("@/lib/utils/organization-data", () => ({
  getEffectiveOwnerId: jest.fn().mockResolvedValue("u1"),
}));
jest.mock("@/lib/services/quotations.service", () => ({
  createQuotation: jest.fn().mockResolvedValue({ id: "q1", title: "Test" }),
  dispatchQuotation: jest.fn().mockResolvedValue({
    quotation: { id: "q1" },
    phoneStatus: "sent",
    emailQueued: true,
  }),
}));
jest.mock("@/lib/csrf", () => ({ withCSRF: (fn: unknown) => fn }));

import { NextRequest } from "next/server";
import { POST } from "@/app/api/dashboard/quotations/route";
import * as quotationsService from "@/lib/services/quotations.service";

function makeRequest(body: Record<string, unknown>) {
  return new NextRequest("http://localhost:9000/api/dashboard/quotations", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
}

describe("POST /api/dashboard/quotations", () => {
  beforeEach(() => jest.clearAllMocks());

  it("calls dispatchQuotation when sendImmediately is true", async () => {
    const req = makeRequest({
      title: "Test",
      items: [],
      sendImmediately: true,
    });
    const res = await POST(req);
    expect(res.status).toBe(201);
    expect(quotationsService.dispatchQuotation).toHaveBeenCalledWith(
      "u1",
      "q1",
    );
  });

  it("does NOT call dispatchQuotation when sendImmediately is false", async () => {
    const req = makeRequest({
      title: "Test",
      items: [],
      sendImmediately: false,
    });
    await POST(req);
    expect(quotationsService.dispatchQuotation).not.toHaveBeenCalled();
  });

  it("returns dispatch info in response when sendImmediately is true", async () => {
    const req = makeRequest({
      title: "Test",
      items: [],
      sendImmediately: true,
    });
    const res = await POST(req);
    const body = await res.json();
    expect(body.phoneStatus).toBe("sent");
    expect(body.emailQueued).toBe(true);
  });
});
```

**Step 2: Run test to verify it fails**

```bash
cd mawidi-site && npm test -- --testPathPattern="create-dispatch" --no-coverage 2>&1 | tail -15
```

Expected: FAIL — `dispatchQuotation` not being called

**Step 3: Update the POST handler**

In `app/api/dashboard/quotations/route.ts`, update the POST handler after the `createQuotation` call:

```typescript
export const POST = withCSRF(async (request: NextRequest) => {
  try {
    const user = await getAuthenticatedUser(request, auth);
    if (!user) {
      return NextResponse.json(
        { success: false, error: "Unauthorized" },
        { status: 401 },
      );
    }

    const effectiveUserId = await getEffectiveOwnerId(user.userId);
    const body = await request.json();
    const parsed = createQuotationSchema.safeParse(body);
    if (!parsed.success) {
      return NextResponse.json(
        {
          success: false,
          error: "Invalid input",
          details: parsed.error.flatten(),
        },
        { status: 400 },
      );
    }

    const quotation = await quotationsService.createQuotation(
      effectiveUserId,
      parsed.data,
    );

    // If sendImmediately, dispatch email + mock phone immediately
    if (parsed.data.sendImmediately) {
      const dispatch = await quotationsService.dispatchQuotation(
        effectiveUserId,
        quotation.id,
      );
      return NextResponse.json(
        {
          success: true,
          quotation: dispatch?.quotation ?? quotation,
          phoneStatus: dispatch?.phoneStatus ?? null,
          emailQueued: dispatch?.emailQueued ?? false,
        },
        { status: 201 },
      );
    }

    return NextResponse.json({ success: true, quotation }, { status: 201 });
  } catch (error) {
    log.error("Failed to create quotation", error);
    return NextResponse.json(
      { success: false, error: "Internal server error" },
      { status: 500 },
    );
  }
});
```

**Step 4: Run test to verify it passes**

```bash
cd mawidi-site && npm test -- --testPathPattern="create-dispatch" --no-coverage 2>&1 | tail -10
```

Expected: PASS

**Step 5: Commit**

```bash
git add mawidi-site/app/api/dashboard/quotations/route.ts \
        mawidi-site/__tests__/api/dashboard/quotations/create-dispatch.test.ts
git commit -m "feat(quotations): wire sendImmediately flag in create endpoint"
```

---

## Task 5: Wire `dispatchQuotation` into the send endpoint

**Files:**

- Modify: `app/api/dashboard/quotations/[id]/send/route.ts`

**Step 1: Update the send endpoint**

Replace `quotationsService.sendQuotation` with `quotationsService.dispatchQuotation`:

```typescript
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { getAuthenticatedUser } from "@/lib/utils/auth-helpers.util";
import { getEffectiveOwnerId } from "@/lib/utils/organization-data";
import { withCSRF } from "@/lib/csrf";
import { logger } from "@/lib/logger";
import * as quotationsService from "@/lib/services/quotations.service";

const log = logger.scope("SendQuotationAPI");

type RouteContext = { params: Promise<{ id: string }> };

export const POST = withCSRF(
  async (request: NextRequest, context: RouteContext) => {
    try {
      const user = await getAuthenticatedUser(request, auth);
      if (!user) {
        return NextResponse.json(
          { success: false, error: "Unauthorized" },
          { status: 401 },
        );
      }

      const effectiveUserId = await getEffectiveOwnerId(user.userId);
      const { id } = await context.params;
      const result = await quotationsService.dispatchQuotation(
        effectiveUserId,
        id,
      );

      if (!result) {
        return NextResponse.json(
          { success: false, error: "Quotation not found" },
          { status: 404 },
        );
      }

      return NextResponse.json({
        success: true,
        quotation: result.quotation,
        phoneStatus: result.phoneStatus,
        emailQueued: result.emailQueued,
      });
    } catch (error) {
      log.error("Failed to send quotation", error);
      return NextResponse.json(
        { success: false, error: "Internal server error" },
        { status: 500 },
      );
    }
  },
);
```

**Step 2: Verify TypeScript**

```bash
cd mawidi-site && npx tsc --noEmit 2>&1 | grep -i "send/route" | head -5
```

Expected: no output

**Step 3: Commit**

```bash
git add mawidi-site/app/api/dashboard/quotations/[id]/send/route.ts
git commit -m "feat(quotations): wire dispatchQuotation into send endpoint"
```

---

## Task 6: Fix Stripe webhook metadata key mismatch (bug)

The portal service creates checkout sessions with `metadata: { quotationId, portalToken, type }` (camelCase), but the webhook handler looks for `session.metadata?.quotation_id` (snake_case). This causes Stripe payments via the portal to never update the quotation status.

**Files:**

- Modify: `lib/services/quotation-payments.service.ts`
- Test: `__tests__/lib/services/quotation-payments-webhook.test.ts` (new)

**Step 1: Write failing test**

Create `mawidi-site/__tests__/lib/services/quotation-payments-webhook.test.ts`:

```typescript
jest.mock("@/lib/db", () => ({
  prisma: {
    quotation_payments: {
      findFirst: jest.fn().mockResolvedValue(null), // not already processed
      create: jest.fn().mockResolvedValue({ id: "pay1" }),
    },
    quotations: {
      findUnique: jest.fn().mockResolvedValue({
        id: "q1",
        total: 500,
        penaltyAmount: 0,
        amountPaid: 0,
        currency: "QAR",
        dueDate: null,
      }),
      update: jest.fn().mockResolvedValue({}),
    },
  },
}));

import { prisma } from "@/lib/db";
import { handleQuotationPaymentWebhook } from "@/lib/services/quotation-payments.service";
import type Stripe from "stripe";

function makeSession(
  metadata: Record<string, string>,
): Stripe.Checkout.Session {
  return {
    id: "cs_test_abc",
    payment_intent: "pi_test_123",
    amount_total: 50000, // 500.00 in fils/cents
    currency: "qar",
    metadata,
  } as unknown as Stripe.Checkout.Session;
}

describe("handleQuotationPaymentWebhook", () => {
  beforeEach(() => jest.clearAllMocks());

  it("processes camelCase quotationId from portal checkout", async () => {
    const session = makeSession({
      type: "quotation_payment",
      quotationId: "q1",
    });
    await handleQuotationPaymentWebhook(session);
    expect(prisma.quotation_payments.create).toHaveBeenCalled();
  });

  it("processes snake_case quotation_id from dashboard checkout", async () => {
    const session = makeSession({
      type: "quotation_payment",
      quotation_id: "q1",
    });
    await handleQuotationPaymentWebhook(session);
    expect(prisma.quotation_payments.create).toHaveBeenCalled();
  });

  it("logs warning and returns early when no quotation id found", async () => {
    const session = makeSession({ type: "quotation_payment" });
    await handleQuotationPaymentWebhook(session);
    expect(prisma.quotation_payments.create).not.toHaveBeenCalled();
  });
});
```

**Step 2: Run test to verify camelCase test fails**

```bash
cd mawidi-site && npm test -- --testPathPattern="quotation-payments-webhook" --no-coverage 2>&1 | tail -15
```

Expected: FAIL — `processes camelCase quotationId from portal checkout` fails

**Step 3: Fix the metadata key lookup**

In `lib/services/quotation-payments.service.ts`, line 130, change:

```typescript
// Before:
const quotationId = session.metadata?.quotation_id;

// After:
const quotationId =
  session.metadata?.quotation_id ?? session.metadata?.quotationId;
```

**Step 4: Run test to verify all pass**

```bash
cd mawidi-site && npm test -- --testPathPattern="quotation-payments-webhook" --no-coverage 2>&1 | tail -10
```

Expected: PASS — 3 tests passing

**Step 5: Commit**

```bash
git add mawidi-site/lib/services/quotation-payments.service.ts \
        mawidi-site/__tests__/lib/services/quotation-payments-webhook.test.ts
git commit -m "fix(quotations): handle both camelCase and snake_case quotation ID in payment webhook"
```

---

## Task 7: Add `defaultSendImmediately` to settings schema, validator, and UI

**Files:**

- Modify: `prisma/schema.prisma`
- Run: `npx prisma migrate dev`
- Modify: `lib/validators/quotation-settings.validator.ts`
- Modify: `components/dashboard/quotations/QuotationSettingsPanel.tsx`

**Step 1: Add field to Prisma schema**

In `prisma/schema.prisma`, inside the `quotation_settings` model (after `reminderSchedule`), add:

```prisma
// Delivery defaults
defaultSendImmediately Boolean @default(true)
```

**Step 2: Run migration**

```bash
cd mawidi-site && npx prisma migrate dev --name add_default_send_immediately
```

Expected: Migration created and applied, `lib/generated/prisma/` regenerated

**Step 3: Add to settings validator**

In `lib/validators/quotation-settings.validator.ts`, add to `updateQuotationSettingsSchema`:

```typescript
defaultSendImmediately: z.boolean().optional(),
```

**Step 4: Add Delivery Defaults section to `QuotationSettingsPanel.tsx`**

a) Add labels to both `t.en` and `t.ar`:

```typescript
// In t.en:
deliveryDefaults: "Delivery Defaults",
defaultSendImmediately: "Send to customer immediately by default",

// In t.ar:
deliveryDefaults: "إعدادات الإرسال",
defaultSendImmediately: "إرسال للعميل فوراً بشكل افتراضي",
```

b) Add `defaultSendImmediately` to the settings state initialization (wherever the settings object is destructured for form state).

c) Add UI section — place it just before the save button, after the Reminders section:

```tsx
{
  /* Delivery Defaults */
}
<div className="border-t pt-6">
  <h3 className="text-sm font-semibold text-gray-900 mb-4">
    {labels.deliveryDefaults}
  </h3>
  <div className="flex items-center justify-between">
    <label className="text-sm text-gray-700">
      {labels.defaultSendImmediately}
    </label>
    <label className="relative inline-flex items-center cursor-pointer">
      <input
        type="checkbox"
        checked={form.defaultSendImmediately ?? true}
        onChange={(e) =>
          setForm((prev) => ({
            ...prev,
            defaultSendImmediately: e.target.checked,
          }))
        }
        className="sr-only peer"
      />
      <div className="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-blue-600" />
    </label>
  </div>
</div>;
```

**Step 5: Verify TypeScript**

```bash
cd mawidi-site && npx tsc --noEmit 2>&1 | grep -i "settingspanel\|setting" | head -10
```

Expected: no errors for the modified files

**Step 6: Commit**

```bash
git add mawidi-site/prisma/schema.prisma \
        mawidi-site/lib/generated/prisma/ \
        mawidi-site/lib/validators/quotation-settings.validator.ts \
        mawidi-site/components/dashboard/quotations/QuotationSettingsPanel.tsx
git commit -m "feat(quotations): add defaultSendImmediately to settings schema and UI"
```

---

## Task 8: Final verification

**Step 1: Run all quotation-related tests**

```bash
cd mawidi-site && npm test -- --testPathPattern="quotation" --no-coverage 2>&1 | tail -25
```

Expected: All pass

**Step 2: Full TypeScript check**

```bash
cd mawidi-site && npx tsc --noEmit 2>&1 | head -20
```

Expected: 0 errors (or same pre-existing errors as before this feature)

**Step 3: Final commit (if any loose files)**

```bash
git status
# If clean, nothing to do. If any files, commit them.
```

---

## Summary of Changes

| File                                                         | What changed                                                                  |
| ------------------------------------------------------------ | ----------------------------------------------------------------------------- |
| `components/dashboard/quotations/QuotationsPanel.tsx`        | Removed Deals Pipeline + Pipeline Stats tabs, state, imports                  |
| `lib/validators/quotations.validator.ts`                     | Added `sendImmediately: boolean` field                                        |
| `components/dashboard/quotations/QuotationForm.tsx`          | Added checkbox + state + label                                                |
| `lib/services/quotations.service.ts`                         | Added `dispatchQuotation` function                                            |
| `app/api/dashboard/quotations/route.ts`                      | Calls `dispatchQuotation` when `sendImmediately`                              |
| `app/api/dashboard/quotations/[id]/send/route.ts`            | Replaced `sendQuotation` with `dispatchQuotation`                             |
| `lib/services/quotation-payments.service.ts`                 | Fixed metadata key: now checks both `quotation_id` and `quotationId`          |
| `prisma/schema.prisma`                                       | Added `defaultSendImmediately Boolean @default(true)` to `quotation_settings` |
| `lib/validators/quotation-settings.validator.ts`             | Added `defaultSendImmediately` field                                          |
| `components/dashboard/quotations/QuotationSettingsPanel.tsx` | Added Delivery Defaults toggle section                                        |
