Initial import of AbelBirdNest Stock

This commit is contained in:
2026-05-16 18:25:51 +07:00
commit 14bb9bf744
472 changed files with 70671 additions and 0 deletions

View File

@ -0,0 +1,169 @@
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;
}
}