task-d.5.6
This commit is contained in:
69
src/app/api/crm/opportunities/[id]/mark-cancelled/route.ts
Normal file
69
src/app/api/crm/opportunities/[id]/mark-cancelled/route.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { opportunityMarkCancelledSchema } from '@/features/crm/opportunities/schemas/lifecycle.schema';
|
||||
import { markOpportunityCancelled } from '@/features/crm/opportunities/server/lifecycle.service';
|
||||
import { getOpportunityDetail } from '@/features/crm/opportunities/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
type Params = {
|
||||
params: Promise<{ id: string }>;
|
||||
};
|
||||
|
||||
export async function POST(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmOpportunityMarkCancelled
|
||||
});
|
||||
|
||||
const payload = opportunityMarkCancelledSchema.parse(await request.json());
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
membershipRole: membership.role,
|
||||
businessRole: membership.businessRole,
|
||||
permissions: membership.permissions ?? [],
|
||||
branchScopeIds: membership.branchScopeIds ?? [],
|
||||
productTypeScopeIds: membership.productTypeScopeIds ?? [],
|
||||
ownershipScope: membership.ownershipScope ?? 'organization',
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
|
||||
const before = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
const updated = await markOpportunityCancelled(
|
||||
id,
|
||||
organization.id,
|
||||
session.user.id,
|
||||
payload,
|
||||
accessContext
|
||||
);
|
||||
const after = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Opportunity marked as cancelled successfully',
|
||||
before,
|
||||
opportunity: updated,
|
||||
after
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ message: error.issues[0]?.message ?? 'Invalid payload' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to mark opportunity as cancelled' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
opportunityMarkLostSchema
|
||||
} from '@/features/crm/opportunities/schemas/opportunity.schema';
|
||||
import { getOpportunityDetail, markOpportunityAsLost } from '@/features/crm/opportunities/server/service';
|
||||
import { opportunityMarkLostSchema } from '@/features/crm/opportunities/schemas/lifecycle.schema';
|
||||
import { markOpportunityLost } from '@/features/crm/opportunities/server/lifecycle.service';
|
||||
import { getOpportunityDetail } from '@/features/crm/opportunities/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
@@ -17,6 +16,7 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmOpportunityMarkLost
|
||||
});
|
||||
|
||||
const payload = opportunityMarkLostSchema.parse(await request.json());
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
@@ -30,8 +30,9 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
|
||||
const before = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
const updated = await markOpportunityAsLost(id, organization.id, session.user.id, payload, accessContext);
|
||||
const updated = await markOpportunityLost(id, organization.id, session.user.id, payload, accessContext);
|
||||
const after = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -60,4 +61,3 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: 'Unable to mark opportunity as lost' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { opportunityMarkNoQuotationSchema } from '@/features/crm/opportunities/schemas/lifecycle.schema';
|
||||
import { markOpportunityNoQuotation } from '@/features/crm/opportunities/server/lifecycle.service';
|
||||
import { getOpportunityDetail } from '@/features/crm/opportunities/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
type Params = {
|
||||
params: Promise<{ id: string }>;
|
||||
};
|
||||
|
||||
export async function POST(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmOpportunityMarkNoQuotation
|
||||
});
|
||||
|
||||
const payload = opportunityMarkNoQuotationSchema.parse(await request.json());
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
membershipRole: membership.role,
|
||||
businessRole: membership.businessRole,
|
||||
permissions: membership.permissions ?? [],
|
||||
branchScopeIds: membership.branchScopeIds ?? [],
|
||||
productTypeScopeIds: membership.productTypeScopeIds ?? [],
|
||||
ownershipScope: membership.ownershipScope ?? 'organization',
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
|
||||
const before = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
const updated = await markOpportunityNoQuotation(
|
||||
id,
|
||||
organization.id,
|
||||
session.user.id,
|
||||
payload,
|
||||
accessContext
|
||||
);
|
||||
const after = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Opportunity marked as no quotation successfully',
|
||||
before,
|
||||
opportunity: updated,
|
||||
after
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ message: error.issues[0]?.message ?? 'Invalid payload' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to mark opportunity as no quotation' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
opportunityMarkWonSchema
|
||||
} from '@/features/crm/opportunities/schemas/opportunity.schema';
|
||||
import { getOpportunityDetail, markOpportunityAsWon } from '@/features/crm/opportunities/server/service';
|
||||
import { opportunityMarkWonSchema } from '@/features/crm/opportunities/schemas/lifecycle.schema';
|
||||
import { markOpportunityWon } from '@/features/crm/opportunities/server/lifecycle.service';
|
||||
import { getOpportunityDetail } from '@/features/crm/opportunities/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
@@ -17,6 +16,7 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmOpportunityMarkWon
|
||||
});
|
||||
|
||||
const payload = opportunityMarkWonSchema.parse(await request.json());
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
@@ -30,8 +30,9 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
|
||||
const before = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
const updated = await markOpportunityAsWon(id, organization.id, session.user.id, payload, accessContext);
|
||||
const updated = await markOpportunityWon(id, organization.id, session.user.id, payload, accessContext);
|
||||
const after = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -60,4 +61,3 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: 'Unable to mark opportunity as won' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { opportunityReopenSchema } from '@/features/crm/opportunities/schemas/opportunity.schema';
|
||||
import { getOpportunityDetail, reopenLostOpportunity } from '@/features/crm/opportunities/server/service';
|
||||
import { opportunityReopenSchema } from '@/features/crm/opportunities/schemas/lifecycle.schema';
|
||||
import { reopenOpportunity } from '@/features/crm/opportunities/server/lifecycle.service';
|
||||
import { getOpportunityDetail } from '@/features/crm/opportunities/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
@@ -13,8 +14,9 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmOpportunityReopen
|
||||
permission: PERMISSIONS.crmOpportunityLifecycleReopen
|
||||
});
|
||||
|
||||
const payload = opportunityReopenSchema.parse(await request.json());
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
@@ -28,8 +30,9 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
|
||||
const before = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
const updated = await reopenLostOpportunity(id, organization.id, session.user.id, payload, accessContext);
|
||||
const updated = await reopenOpportunity(id, organization.id, session.user.id, payload, accessContext);
|
||||
const after = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -58,4 +61,3 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: 'Unable to reopen opportunity' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditAction, auditCreate } from '@/features/foundation/audit-log/service';
|
||||
import { createOpportunity, listOpportunities } from '@/features/crm/opportunities/server/service';
|
||||
import { getCustomerDetail } from '@/features/crm/customers/server/service';
|
||||
import { opportunitySchema } from '@/features/crm/opportunities/schemas/opportunity.schema';
|
||||
import { createOpportunity, listOpportunities } from '@/features/crm/opportunities/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
@@ -15,56 +15,62 @@ const opportunityRequestSchema = opportunitySchema.extend({
|
||||
expectedCloseDate: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
function buildAccessContext(
|
||||
organizationId: string,
|
||||
sessionUserId: string,
|
||||
membership: Awaited<ReturnType<typeof requireOrganizationAccess>>['membership']
|
||||
) {
|
||||
return {
|
||||
organizationId,
|
||||
userId: sessionUserId,
|
||||
membershipRole: membership.role,
|
||||
businessRole: membership.businessRole,
|
||||
permissions: membership.permissions ?? [],
|
||||
branchScopeIds: membership.branchScopeIds ?? [],
|
||||
productTypeScopeIds: membership.productTypeScopeIds ?? [],
|
||||
ownershipScope: membership.ownershipScope ?? 'organization',
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmOpportunityRead
|
||||
});
|
||||
|
||||
const { searchParams } = request.nextUrl;
|
||||
const accessContext = buildAccessContext(organization.id, session.user.id, membership);
|
||||
const page = Number(searchParams.get('page') ?? 1);
|
||||
const limit = Number(searchParams.get('limit') ?? 10);
|
||||
const search = searchParams.get('search') ?? undefined;
|
||||
const pipelineStage = searchParams.get('pipelineStage') ?? undefined;
|
||||
const status = searchParams.get('status') ?? undefined;
|
||||
const productType = searchParams.get('productType') ?? undefined;
|
||||
const priority = searchParams.get('priority') ?? undefined;
|
||||
const branch = searchParams.get('branch') ?? undefined;
|
||||
const customer = searchParams.get('customer') ?? undefined;
|
||||
const sort = searchParams.get('sort') ?? undefined;
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
membershipRole: membership.role,
|
||||
businessRole: membership.businessRole,
|
||||
branchScopeIds: membership.branchScopeIds ?? [],
|
||||
productTypeScopeIds: membership.productTypeScopeIds ?? [],
|
||||
ownershipScope: membership.ownershipScope ?? 'organization',
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
const result = await listOpportunities(
|
||||
accessContext,
|
||||
{
|
||||
page,
|
||||
limit,
|
||||
search,
|
||||
pipelineStage: pipelineStage as 'lead' | 'opportunity' | 'closed_won' | 'closed_lost' | undefined,
|
||||
status,
|
||||
productType,
|
||||
priority,
|
||||
branch,
|
||||
customer,
|
||||
sort
|
||||
}
|
||||
);
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const result = await listOpportunities(accessContext, {
|
||||
page,
|
||||
limit,
|
||||
search: searchParams.get('search') ?? undefined,
|
||||
pipelineStage:
|
||||
(searchParams.get('pipelineStage') as 'lead' | 'opportunity' | 'closed_won' | 'closed_lost' | null) ??
|
||||
undefined,
|
||||
status: searchParams.get('status') ?? undefined,
|
||||
stage: searchParams.get('stage') ?? undefined,
|
||||
outcomeStatus: searchParams.get('outcomeStatus') ?? undefined,
|
||||
productType: searchParams.get('productType') ?? undefined,
|
||||
priority: searchParams.get('priority') ?? undefined,
|
||||
branch: searchParams.get('branch') ?? undefined,
|
||||
customer: searchParams.get('customer') ?? undefined,
|
||||
lostReason: searchParams.get('lostReason') ?? undefined,
|
||||
closedDateFrom: searchParams.get('closedDateFrom') ?? undefined,
|
||||
closedDateTo: searchParams.get('closedDateTo') ?? undefined,
|
||||
sort: searchParams.get('sort') ?? undefined
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Opportunities loaded successfully',
|
||||
message: 'Success',
|
||||
totalItems: result.totalItems,
|
||||
offset,
|
||||
offset: (page - 1) * limit,
|
||||
limit,
|
||||
items: result.items
|
||||
});
|
||||
@@ -77,7 +83,7 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to load opportunities' }, { status: 500 });
|
||||
return NextResponse.json({ message: 'Unable to list opportunities' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,24 +92,10 @@ export async function POST(request: NextRequest) {
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmOpportunityCreate
|
||||
});
|
||||
|
||||
const payload = opportunityRequestSchema.parse(await request.json());
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
membershipRole: membership.role,
|
||||
businessRole: membership.businessRole,
|
||||
branchScopeIds: membership.branchScopeIds ?? [],
|
||||
productTypeScopeIds: membership.productTypeScopeIds ?? [],
|
||||
ownershipScope: membership.ownershipScope ?? 'organization',
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
const created = await createOpportunity(
|
||||
organization.id,
|
||||
session.user.id,
|
||||
accessContext,
|
||||
payload
|
||||
);
|
||||
const accessContext = buildAccessContext(organization.id, session.user.id, membership);
|
||||
const created = await createOpportunity(organization.id, session.user.id, accessContext, payload);
|
||||
const customer = await getCustomerDetail(payload.customerId, organization.id);
|
||||
|
||||
await auditCreate({
|
||||
@@ -161,4 +153,3 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ message: 'Unable to create opportunity' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user