Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
49067f1
chore(ci): add SonarQube Cloud static-analysis + coverage scan
teetangh Jul 10, 2026
a4d4912
chore(ci): pre-fill sonar.organization=practitionist
teetangh Jul 10, 2026
22fdfe0
chore(ci): scope Sonar PR to Automatic Analysis config only
teetangh Jul 10, 2026
ee95455
feat(moderation): wire real side-effects into staff moderation action…
teetangh Jul 11, 2026
fde3490
fix(stream): authz guards on token providers and addMemberToChannel (…
teetangh Jul 11, 2026
de0b0bc
perf(webhooks): narrow notification fetches from deep includes to sel…
teetangh Jul 11, 2026
e9471ae
chore(tasks)+docs: retire the tasks/ folder; record moderation + peer…
teetangh Jul 11, 2026
49ea52b
fix(moderation): CI type errors — use extended-client Tx type, es5-sa…
teetangh Jul 11, 2026
e154607
fix(moderation): review-comment triage — clearable suspension input, …
teetangh Jul 11, 2026
5987e85
fix(moderation): CodeRabbit triage — honor Novu TriggerResult, surfac…
teetangh Jul 11, 2026
ece05d6
docs(bugs): add CTO subsystem architecture audit pack
teetangh Jul 11, 2026
c446e16
docs(bugs): recommend A/B/C options with why and why-not
teetangh Jul 11, 2026
d8af6da
docs(bugs): file P0 Stream recording storage scale infrastructure
teetangh Jul 11, 2026
f041688
docs(bugs): deepen enterprise taxonomy, money trust, and gaming audits
teetangh Jul 11, 2026
ea5749f
fix(stream): session-bind token actions and demote default Stream role
teetangh Jul 11, 2026
665ca5b
fix(enterprise): enforce ADR-18 program allowlist + exclusive-engagem…
teetangh Jul 11, 2026
36dea1e
fix(reviews): enforce review ownership + booking eligibility, unique …
teetangh Jul 11, 2026
62a41e1
Merge pull request #980 from Practitionist/backmerge/prod-to-dev-2026…
teetangh Jul 11, 2026
d400e4b
fix(stream): grant channel-scoped host/moderator on event + meeting c…
teetangh Jul 11, 2026
a76e371
fix(stream): enqueue permanent-recording transfer on ready, stream up…
teetangh Jul 11, 2026
183d0e7
chore(payments): remove Lemon Squeezy + XFlow, add post-MVP DODO_PAYM…
teetangh Jul 11, 2026
0539a30
feat(waitlist): soft-hold seat on accept, transactional join with fri…
teetangh Jul 11, 2026
ebd9f86
fix(booking): scope subscription reschedule status, wire allocation i…
teetangh Jul 11, 2026
fe8d808
fix(trust): referral stash (#891), Novu idempotency, support dedup, c…
teetangh Jul 11, 2026
043550f
fix(payments): auto-refund amount-mismatch + double-booking loser, fr…
teetangh Jul 11, 2026
5e171b0
fix(enterprise): CAS onboarding submit + SSO settings, gate SCIM seat…
teetangh Jul 11, 2026
a663356
fix(enterprise): rescope PENDING_TRUST to sponsor org, park consultan…
teetangh Jul 11, 2026
3ed84ce
feat(booking): consultant no-show detection with auto-refund and noti…
teetangh Jul 11, 2026
1ce9888
feat(payments): add BATCHED earnings status so PAID means cash moved
teetangh Jul 11, 2026
44a52a9
fix(payments): close the checkout↔ledger earnings-journal consistency…
teetangh Jul 11, 2026
5e0e7ca
docs(bugs): stamp triage verdicts and fix-PR mapping across the audit…
teetangh Jul 11, 2026
eeb72f7
fix(booking): address review — guard undefined appointmentId filter, …
teetangh Jul 11, 2026
5f5fe88
fix(trust): address review — strip internal fields from ticket dedup …
teetangh Jul 11, 2026
08a8843
fix(payments): address review — add WalletFrozenError httpStatus
teetangh Jul 11, 2026
cc55e96
fix(payments): address review — BigInt-safe Sentry context, clear man…
teetangh Jul 11, 2026
0dd75bf
fix(booking): address review — reconcile detector excludes in-flight …
teetangh Jul 11, 2026
c45f17b
fix(reviews): address review — validate PUT body, serialize rating re…
teetangh Jul 11, 2026
a89117a
fix(stream): address review — don't grant channel-moderator on direct…
teetangh Jul 11, 2026
44b4f12
fix(stream): address review — make recording enqueue durable, bound r…
teetangh Jul 11, 2026
34fb1cc
fix(enterprise): address review — always bump SSO version, idempotent…
teetangh Jul 11, 2026
bf57e0a
Merge remote-tracking branch 'origin/fix/enterprise-invoice-trust' in…
teetangh Jul 11, 2026
57bfd00
Merge remote-tracking branch 'origin/feat/payout-batched-status' into…
teetangh Jul 11, 2026
c376d09
Merge remote-tracking branch 'origin/fix/checkout-earnings-atomicity'…
teetangh Jul 11, 2026
fef5643
Merge remote-tracking branch 'origin/feat/noshow-automation' into int…
teetangh Jul 11, 2026
970c03e
Merge remote-tracking branch 'origin/fix/booking-correctness' into in…
teetangh Jul 11, 2026
4d935ff
Merge remote-tracking branch 'origin/fix/reviews-integrity' into inte…
teetangh Jul 11, 2026
cd5a27a
Merge remote-tracking branch 'origin/chore/gateway-cleanup' into inte…
teetangh Jul 11, 2026
c4fbbc6
Merge remote-tracking branch 'origin/fix/onboarding-identity-races' i…
teetangh Jul 11, 2026
2405974
Merge remote-tracking branch 'origin/fix/trust-safety-sweep' into int…
teetangh Jul 11, 2026
379b78f
Merge remote-tracking branch 'origin/fix/stream-security' into integr…
teetangh Jul 11, 2026
103cf5d
Merge remote-tracking branch 'origin/fix/adr18-allowlist-enforcement'…
teetangh Jul 11, 2026
0b38132
Merge remote-tracking branch 'origin/feat/waitlist-seat-hold' into in…
teetangh Jul 11, 2026
2613e83
Merge remote-tracking branch 'origin/fix/recordings-pipeline' into in…
teetangh Jul 11, 2026
d4a6e68
docs: reconcile documentation with the CTO audit fix wave
teetangh Jul 11, 2026
44205cd
fix(integration): resolve type errors from the merge — narrow checkou…
teetangh Jul 11, 2026
85a8f35
fix(integration): address review — INVOICE-gate PENDING_TRUST, surfac…
teetangh Jul 11, 2026
6accddb
test(integration): update booking/payments/payout tests for the fix-w…
teetangh Jul 12, 2026
2289847
Merge remote-tracking branch 'origin/dev' into HEAD
teetangh Jul 16, 2026
c252a80
docs(bugs): markdownlint + formatting fixes from review
teetangh Jul 16, 2026
6a2cb59
fix(moderation): unblock next build for /api/admin/analytics + update…
teetangh Jul 16, 2026
888a7ae
fix(auth): define STAFF/ADMIN admin roles so the BetterAuth admin plu…
teetangh Jul 16, 2026
ed0f994
revert(moderation): drop unnecessary force-dynamic on analytics route
teetangh Jul 16, 2026
431a823
merge(#974): reconcile moderation branch into the audit wave
teetangh Jul 16, 2026
d126d30
docs+chore(moderation): clarify sideEffects recovery guarantee, Numbe…
teetangh Jul 16, 2026
b58923a
Merge remote-tracking branch 'origin/feature/moderation-actions-693' …
teetangh Jul 16, 2026
8776d31
fix(novu): give toSorted a localeCompare comparator (Sonar S2871)
teetangh Jul 16, 2026
d3e3161
chore(moderation): Number.parseInt (Sonar S7773)
teetangh Jul 16, 2026
0ee07a0
chore(sonar): node:fs import + Number.parseInt (S7772/S7773)
teetangh Jul 16, 2026
48fda59
fix(moderation): reject decimal suspension durations instead of trunc…
teetangh Jul 16, 2026
7c96868
refactor(sonar): reduce cognitive complexity in SSO/onboarding/cleanu…
teetangh Jul 16, 2026
fb3fbfc
Merge remote-tracking branch 'origin/feature/moderation-actions-693' …
teetangh Jul 16, 2026
1d36220
refactor(moderation): reduce cognitive complexity in side-effects/act…
teetangh Jul 16, 2026
7a93861
Merge remote-tracking branch 'origin/feature/moderation-actions-693' …
teetangh Jul 16, 2026
ef1a2df
Merge PR #974: moderation side-effects (#693) + Stream authz
teetangh Jul 16, 2026
a637b8e
Merge PR #995: CTO Sub-Audit Fix Wave + moderation (#693) + Sonar cle…
teetangh Jul 16, 2026
61ec11d
Merge branch 'dev' into bugs/cto-subsystem-audit
teetangh Jul 16, 2026
eaefde2
Merge PR #976: CTO subsystem audit pack (annotated)
teetangh Jul 16, 2026
fcfc4ac
Merge remote-tracking branch 'origin/dev' into chore/sonarqube-cloud-…
teetangh Jul 17, 2026
27e7a70
chore(ci): make Sonar exclusions apply via .sonarcloud.properties
teetangh Jul 17, 2026
7362fe3
Merge PR #967: SonarCloud config (.sonarcloud.properties exclusions +…
teetangh Jul 17, 2026
e4cb92d
fix(booking): scheduling-timezone limit buckets, guarded + idempotent…
teetangh Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/cleanup-abandoned-payments.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ jobs:
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
RAZORPAY_KEY_ID: ${{ secrets.RAZORPAY_KEY_ID }}
RAZORPAY_SECRET: ${{ secrets.RAZORPAY_SECRET }}
LEMON_SQUEEZY_API_KEY: ${{ secrets.LEMON_SQUEEZY_API_KEY }}
XFLOW_SECRET_KEY: ${{ secrets.XFLOW_SECRET_KEY }}

steps:
- name: Checkout code
Expand Down
53 changes: 53 additions & 0 deletions .github/workflows/detect-consultant-no-shows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Detect Consultant No-Shows

on:
schedule:
# Run hourly (offset to avoid colliding with other :00/:07 crons)
- cron: "17 * * * *"
workflow_dispatch: # Allow manual triggering

jobs:
detect-consultant-no-shows:
runs-on: ubuntu-latest
timeout-minutes: 10

env:
# Database connection (required for Prisma)
DATABASE_URL: ${{ secrets.DATABASE_URL }}
DIRECT_URL: ${{ secrets.DIRECT_URL }}
# #476 cron locks load lib/redis at import — every job entry needs these
UPSTASH_REDIS_REST_URL: ${{ secrets.UPSTASH_REDIS_REST_URL }}
UPSTASH_REDIS_REST_TOKEN: ${{ secrets.UPSTASH_REDIS_REST_TOKEN }}
# #471 auto-refund reuses refundPayment (#990) — gateway creds for parity
# with the other money jobs (reconcile-pending-refunds).
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
RAZORPAY_KEY_ID: ${{ secrets.RAZORPAY_KEY_ID }}
RAZORPAY_SECRET: ${{ secrets.RAZORPAY_SECRET }}
# Both-party no-show + refund notifications
NOVU_SECRET_KEY: ${{ secrets.NOVU_SECRET_KEY }}
NEXT_PUBLIC_APP_URL: ${{ secrets.NEXT_PUBLIC_APP_URL }}

steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: "22"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Generate Prisma client
run: npx prisma generate

- name: Detect consultant no-shows
run: npx tsx jobs/appointments/detect-consultant-no-shows.ts

- name: Notify on failure
if: failure()
env:
SLACK_OPS_WEBHOOK_URL: ${{ secrets.SLACK_OPS_WEBHOOK_URL }}
run: bash scripts/ci/notify-ops-failure.sh "detect-consultant-no-shows"
6 changes: 6 additions & 0 deletions .sonarcloud.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# SonarQube Cloud Automatic Analysis reads THIS file (`.sonarcloud.properties`),
# not `sonar-project.properties` (that one is for the future CI-based scan).
# Scope Automatic Analysis to app source: exclude generated output, vendored
# code, type declarations, seeds/migrations, and non-source assets so they don't
# inflate the issue/duplication counts. Coverage still requires a CI scan.
sonar.exclusions=**/node_modules/**,.next/**,coverage/**,**/*.d.ts,prisma/migrations/**,prisma/seedFiles/**,public/**,emails/**,docs/**,load-tests/**,testsprite_tests/**
24 changes: 14 additions & 10 deletions __tests__/booking-algorithm/allocationAlgorithms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Covers:
* - manualAllocate (validation, business rules, error handling)
* - autoAllocate (all strategies, preference filtering, error handling)
* - preAllocate (validation, delegation)
* - allocateRequestedSlots (validation, delegation; formerly preAllocate)
* - filterSlotsByPreferences (private, tested via autoAllocate)
* - allocateConsultationSlots (session duration fix verification)
* - allocateWebinarSlots (consecutive slot finding)
Expand All @@ -20,7 +20,7 @@
type AllocationOptions,
} from "@/app/dashboard/consultant/[consultantId]/(features)/shared/utils/allocationAlgorithms";
import { AllocationService } from "@/app/dashboard/consultant/[consultantId]/(features)/shared/utils/allocationService";
import {

Check failure on line 23 in __tests__/booking-algorithm/allocationAlgorithms.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Mocks should not be manually imported from a __mocks__ directory. Instead use `jest.mock` and import from the original module path
makeTimeSlot,
makeConsecutiveTimeSlots,
makeWeekOfAvailability,
Expand Down Expand Up @@ -398,8 +398,8 @@

if (result.success) {
// Should have distributed across weeks
expect(result.selectedSlots.length).toBeGreaterThan(0);

Check failure on line 401 in __tests__/booking-algorithm/allocationAlgorithms.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Avoid calling `expect` conditionally`
expect(result.strategy).toBe("optimal-distribution");

Check failure on line 402 in __tests__/booking-algorithm/allocationAlgorithms.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Avoid calling `expect` conditionally`
}
});

Expand Down Expand Up @@ -533,8 +533,8 @@
// Morning preference should only include 9-12 slots
result.selectedSlots.forEach((s) => {
const hour = s.startTime.getHours();
expect(hour).toBeGreaterThanOrEqual(9);

Check failure on line 536 in __tests__/booking-algorithm/allocationAlgorithms.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Avoid calling `expect` conditionally`
expect(hour).toBeLessThan(12);

Check failure on line 537 in __tests__/booking-algorithm/allocationAlgorithms.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Avoid calling `expect` conditionally`
});
}
});
Expand Down Expand Up @@ -606,11 +606,11 @@
});
});

// ─── preAllocate ────────────────────────────────────────────────────────────
// ─── allocateRequestedSlots ─────────────────────────────────────────────────

describe("AllocationAlgorithms.preAllocate", () => {
describe("AllocationAlgorithms.allocateRequestedSlots", () => {
it("should reject when no requested slots provided", async () => {
const result = await AllocationAlgorithms.preAllocate({
const result = await AllocationAlgorithms.allocateRequestedSlots({
eventType: "consultation",
eventId: "event-1",
durationInHours: 1,
Expand All @@ -620,7 +620,7 @@
});

it("should reject when requested slots is empty array", async () => {
const result = await AllocationAlgorithms.preAllocate({
const result = await AllocationAlgorithms.allocateRequestedSlots({
eventType: "consultation",
eventId: "event-1",
durationInHours: 1,
Expand All @@ -631,7 +631,7 @@

it("should reject wrong number of requested slots", async () => {
const slots = makeFutureConsecutiveSlots("2025-06-01T09:00:00Z", 1);
const result = await AllocationAlgorithms.preAllocate({
const result = await AllocationAlgorithms.allocateRequestedSlots({
eventType: "consultation",
eventId: "event-1",
durationInHours: 1,
Expand All @@ -643,7 +643,7 @@

it("should succeed with correct number of slots", async () => {
const slots = makeFutureConsecutiveSlots("2025-06-01T09:00:00Z", 2);
const result = await AllocationAlgorithms.preAllocate({
const result = await AllocationAlgorithms.allocateRequestedSlots({
eventType: "consultation",
eventId: "event-1",
durationInHours: 1,
Expand All @@ -655,7 +655,11 @@
"consultation",
"event-1",
slots,
{ useRequestedSlots: true },
{
useRequestedSlots: true,
idempotencyKey: undefined,
initialAllocation: undefined,
},
);
});

Expand All @@ -666,7 +670,7 @@
});

const slots = makeFutureConsecutiveSlots("2025-06-01T09:00:00Z", 2);
const result = await AllocationAlgorithms.preAllocate({
const result = await AllocationAlgorithms.allocateRequestedSlots({
eventType: "consultation",
eventId: "event-1",
durationInHours: 1,
Expand All @@ -680,7 +684,7 @@
mockAllocateSlots.mockRejectedValue(new Error("Connection failed"));

const slots = makeFutureConsecutiveSlots("2025-06-01T09:00:00Z", 2);
const result = await AllocationAlgorithms.preAllocate({
const result = await AllocationAlgorithms.allocateRequestedSlots({
eventType: "consultation",
eventId: "event-1",
durationInHours: 1,
Expand Down
3 changes: 2 additions & 1 deletion __tests__/booking-algorithm/calendarUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
type Appointment,
} from "@/app/dashboard/consultant/[consultantId]/(features)/shared/utils/calendarUtils";
import { ScheduleType, DayOfWeek, AppointmentsType } from "@prisma/client";
import {

Check failure on line 43 in __tests__/booking-algorithm/calendarUtils.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Mocks should not be manually imported from a __mocks__ directory. Instead use `jest.mock` and import from the original module path
makeTimeSlot,
makeConsecutiveTimeSlots,
makeWeeklyAvailabilitySlot,
Expand Down Expand Up @@ -97,7 +97,7 @@
const slots = mapWeeklySlots(data as any, new Date("2025-01-06"), "week");
const mondaySlots = slots.filter((s) => s.startTime.getUTCDay() === 1);
if (mondaySlots.length > 0) {
expect(mondaySlots[0].startTime.getUTCHours()).toBe(14);

Check failure on line 100 in __tests__/booking-algorithm/calendarUtils.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Avoid calling `expect` conditionally`
}
});

Expand Down Expand Up @@ -438,7 +438,8 @@

it("startOfWeekSunday should return Sunday", () => {
const result = startOfWeekSunday(new Date("2025-01-08")); // Wednesday
expect(result.getDay()).toBe(0);
// UTC weekday — this helper is UTC-based; local getDay() shifts by machine TZ
expect(result.getUTCDay()).toBe(0);
});
});

Expand Down
91 changes: 91 additions & 0 deletions __tests__/booking-algorithm/idempotency-key.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Idempotency-Key lifecycle for allocation attempts. A retry of the SAME
* payload must reuse the key (the server replays the original batch, #837);
* any change to mode, event, or slots must mint a fresh key.
*/

import "./setup";

import {
computeAttemptFingerprint,
resolveAttemptKey,
} from "@/app/dashboard/consultant/[consultantId]/(features)/shared/hooks/useSlotAllocation";
// eslint-disable-next-line jest/no-mocks-import -- shared fixture builders, not module mocks (suite-wide pattern)
import { makeConsecutiveTimeSlots } from "./__mocks__/booking.mockData";
import type { TimeSlot } from "@/app/dashboard/consultant/[consultantId]/(features)/shared/utils/calendarUtils";

const slots = makeConsecutiveTimeSlots(
"2026-08-03T09:00:00.000Z",
2,
) as TimeSlot[];

describe("computeAttemptFingerprint", () => {
it("is stable regardless of slot order", () => {
const reversed = [...slots].reverse();
expect(computeAttemptFingerprint("manual", "e1", slots)).toBe(
computeAttemptFingerprint("manual", "e1", reversed),
);
});

it("differs across modes, events, and slot sets", () => {
const fp = computeAttemptFingerprint("manual", "e1", slots);
expect(computeAttemptFingerprint("auto", "e1", slots)).not.toBe(fp);
expect(computeAttemptFingerprint("manual", "e2", slots)).not.toBe(fp);
expect(
computeAttemptFingerprint(
"manual",
"e1",
makeConsecutiveTimeSlots("2026-08-04T09:00:00.000Z", 2) as TimeSlot[],
),
).not.toBe(fp);
});
});

describe("resolveAttemptKey", () => {
it("reuses the key for an identical retry", () => {
const fp = computeAttemptFingerprint("manual", "e1", slots);
const first = resolveAttemptKey(null, fp);
const retry = resolveAttemptKey(first, fp);
expect(retry.key).toBe(first.key);
});

it("mints a new key when the payload changes", () => {
const first = resolveAttemptKey(
null,
computeAttemptFingerprint("manual", "e1", slots),
);
const changed = resolveAttemptKey(
first,
computeAttemptFingerprint("manual", "e1", [
...slots,
...(makeConsecutiveTimeSlots(
"2026-08-05T09:00:00.000Z",
2,
) as TimeSlot[]),
]),
);
expect(changed.key).not.toBe(first.key);
});

it("mints a new key when the mode changes", () => {
const manual = resolveAttemptKey(
null,
computeAttemptFingerprint("manual", "e1", slots),
);
const auto = resolveAttemptKey(
manual,
computeAttemptFingerprint("auto", "e1", []),
);
expect(auto.key).not.toBe(manual.key);
});

it("keys look like UUIDs", () => {
const attempt = resolveAttemptKey(
null,
computeAttemptFingerprint("auto", "e1", []),
);
expect(attempt.key).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
);
});
});
Loading
Loading