170 lines
5.5 KiB
TypeScript
170 lines
5.5 KiB
TypeScript
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
|
import { NextResponse } from "next/server";
|
|
|
|
import {
|
|
AGENT_BALANCE_DIRECTIONS,
|
|
AGENT_BALANCE_SOURCES,
|
|
AGENT_BALANCE_TYPES,
|
|
createAgentBalanceMutation
|
|
} from "@/features/agents/lib/balance-mutations";
|
|
import { serializeAgent } from "@/features/agents/lib/serialize-agent";
|
|
import { agentInputSchema } from "@/features/agents/schemas/agent.schema";
|
|
import { createAuditTrailSafe } from "@/lib/audit-trail";
|
|
import { resolveMasterCode } from "@/lib/master-code";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { requireApiAccess } from "@/lib/authorization";
|
|
|
|
export async function GET(request: Request) {
|
|
const auth = requireApiAccess(request);
|
|
if (!auth.ok) return auth.response;
|
|
|
|
const data = await prisma.agent.findMany({
|
|
include: {
|
|
profitShareScheme: true,
|
|
bankAccounts: {
|
|
include: {
|
|
bank: true
|
|
}
|
|
}
|
|
},
|
|
orderBy: [{ createdAt: "desc" }]
|
|
});
|
|
|
|
return NextResponse.json({ data: data.map(serializeAgent) });
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
const auth = requireApiAccess(request);
|
|
if (!auth.ok) return auth.response;
|
|
|
|
const parsed = agentInputSchema.safeParse(await request.json());
|
|
if (!parsed.success) {
|
|
return NextResponse.json(
|
|
{ message: "Validasi gagal", errors: parsed.error.flatten().fieldErrors },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
try {
|
|
const profitShareSchemeId = BigInt(parsed.data.profit_share_scheme_id);
|
|
const bankIds = parsed.data.bank_accounts.map((account) => BigInt(account.bank_id));
|
|
|
|
const [scheme, banks] = await Promise.all([
|
|
prisma.profitShareScheme.findFirst({
|
|
where: { id: profitShareSchemeId, status: "ACTIVE" }
|
|
}),
|
|
prisma.bank.findMany({
|
|
where: { id: { in: bankIds }, status: "ACTIVE" }
|
|
})
|
|
]);
|
|
|
|
if (!scheme) {
|
|
return NextResponse.json(
|
|
{ message: "Validasi gagal", errors: { profit_share_scheme_id: ["Skema bagi hasil harus dipilih dari master aktif"] } },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
if (banks.length !== bankIds.length) {
|
|
return NextResponse.json(
|
|
{ message: "Validasi gagal", errors: { bank_accounts: ["Semua rekening harus memilih bank aktif dari master"] } },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const resolvedCode = await resolveMasterCode({
|
|
role: auth.user.role,
|
|
prefix: "AGT",
|
|
requestedCode: parsed.data.code,
|
|
countExisting: () => prisma.agent.count({ where: { code: { startsWith: "AGT" } } }),
|
|
exists: async (code) => (await prisma.agent.count({ where: { code } })) > 0
|
|
});
|
|
|
|
if (!resolvedCode.ok) {
|
|
return NextResponse.json(
|
|
{ message: "Validasi gagal", errors: { code: [resolvedCode.message] } },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const joinDate = new Date(parsed.data.join_date);
|
|
const agent = await prisma.$transaction(async (tx) => {
|
|
const created = await tx.agent.create({
|
|
data: {
|
|
code: resolvedCode.code,
|
|
name: parsed.data.name,
|
|
identityType: parsed.data.identity_type,
|
|
identityNumber: parsed.data.identity_number,
|
|
mobilePhone: parsed.data.mobile_phone || null,
|
|
email: parsed.data.email || null,
|
|
address: parsed.data.address || null,
|
|
notes: parsed.data.notes || null,
|
|
joinDate,
|
|
profitShareSchemeId,
|
|
currentBalance: parsed.data.profit_share_balance,
|
|
capitalBalance: parsed.data.capital_balance,
|
|
bankAccounts: {
|
|
create: parsed.data.bank_accounts.map((account) => ({
|
|
bankId: BigInt(account.bank_id),
|
|
accountNumber: account.account_number
|
|
}))
|
|
}
|
|
},
|
|
include: {
|
|
profitShareScheme: true,
|
|
bankAccounts: { include: { bank: true } }
|
|
}
|
|
});
|
|
|
|
if (parsed.data.profit_share_balance > 0) {
|
|
await createAgentBalanceMutation(tx, {
|
|
agentId: created.id,
|
|
balanceType: AGENT_BALANCE_TYPES.PROFIT_SHARE,
|
|
direction: AGENT_BALANCE_DIRECTIONS.IN,
|
|
source: AGENT_BALANCE_SOURCES.OPENING_BALANCE,
|
|
amount: parsed.data.profit_share_balance,
|
|
balanceAfter: parsed.data.profit_share_balance,
|
|
effectiveDate: joinDate,
|
|
notes: "Saldo bagi hasil awal agent"
|
|
});
|
|
}
|
|
|
|
if (parsed.data.capital_balance > 0) {
|
|
await createAgentBalanceMutation(tx, {
|
|
agentId: created.id,
|
|
balanceType: AGENT_BALANCE_TYPES.CAPITAL,
|
|
direction: AGENT_BALANCE_DIRECTIONS.IN,
|
|
source: AGENT_BALANCE_SOURCES.OPENING_BALANCE,
|
|
amount: parsed.data.capital_balance,
|
|
balanceAfter: parsed.data.capital_balance,
|
|
effectiveDate: joinDate,
|
|
notes: "Saldo modal awal agent"
|
|
});
|
|
}
|
|
|
|
return created;
|
|
});
|
|
|
|
await createAuditTrailSafe({
|
|
userId: auth.user.id,
|
|
action: "AGENT_CREATED",
|
|
entityType: "AGENT",
|
|
entityId: agent.id,
|
|
method: request.method,
|
|
pathname: new URL(request.url).pathname,
|
|
statusCode: 201,
|
|
summary: `Agent ${agent.code} dibuat`
|
|
});
|
|
|
|
return NextResponse.json({ data: serializeAgent(agent) }, { status: 201 });
|
|
} catch (error) {
|
|
if (error instanceof PrismaClientKnownRequestError && error.code === "P2002") {
|
|
return NextResponse.json(
|
|
{ message: "Validasi gagal", errors: { identity_number: ["Kode agen atau identitas sudah dipakai"] } },
|
|
{ status: 409 }
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|