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,347 @@
import { NextResponse } from "next/server";
import {
purchaseAnalysisInputSchema,
type PurchaseAnalysisInput
} from "@/features/purchase-analysis/schemas/purchase-analysis.schema";
import { serializePurchaseAnalysisDetail } from "@/features/purchase-analysis/lib/serialize-purchase-analysis";
import { createAuditTrailSafe } from "@/lib/audit-trail";
import { buildAuditChangeMetadata } from "@/lib/audit-trail-diff";
import { requireApiAccess } from "@/lib/authorization";
import { prisma } from "@/lib/prisma";
type RouteContext = {
params: Promise<{
purchaseId: string;
}>;
};
const analysisInclude = {
agent: {
select: {
id: true,
name: true
}
},
profitShareScheme: {
select: {
shareAgent: true
}
},
lines: {
select: {
subtotal: true,
qtyOrdered: true,
qtyReceived: true,
qtyAccepted: true,
unitPrice: true,
malUnitPrice: true,
moistureReceivedPercent: true,
purchaseMoisturePercent: true,
marketReferencePrice: true,
unit: {
select: {
code: true
}
}
}
},
lots: {
select: {
originalQty: true,
finalMoisturePercent: true,
aboveAverageRatioPercent: true,
unit: {
select: {
code: true
}
}
}
},
analysis: {
include: {
costEntries: true
}
}
} as const;
function parseId(rawId: string) {
try {
return BigInt(rawId);
} catch {
return null;
}
}
function toDecimal(value: number | null | undefined) {
if (value === undefined || value === null) return null;
return value;
}
async function upsertAnalysis(purchaseId: bigint, payload: PurchaseAnalysisInput) {
return prisma.purchaseAnalysis.upsert({
where: { purchaseId },
update: {
status: payload.status,
weightBuy: toDecimal(payload.weight_buy),
weightReceived: toDecimal(payload.weight_received),
weightFinal: toDecimal(payload.weight_final),
moistureBuyPercent: toDecimal(payload.moisture_buy_percent),
moistureReceivedPercent: toDecimal(payload.moisture_received_percent),
moistureFinalPercent: toDecimal(payload.moisture_final_percent),
aboveAverageRatioPercent: toDecimal(payload.above_average_ratio_percent),
averagePrice: toDecimal(payload.average_price),
modalBeli: toDecimal(payload.modal_beli),
modalMasuk: toDecimal(payload.modal_masuk),
modalJual: toDecimal(payload.modal_jual),
modalBarang: toDecimal(payload.modal_barang),
totalModalBeli: toDecimal(payload.total_modal_beli),
totalModalMal: toDecimal(payload.total_modal_mal),
marketReferencePrice: toDecimal(payload.market_reference_price),
marketValuationTotal: toDecimal(payload.market_valuation_total),
agentProfitShareTotal: payload.agent_profit_share_total,
notes: payload.notes || null,
costEntries: {
deleteMany: {},
create: payload.cost_entries.map((entry) => ({
costType: entry.cost_type,
description: entry.description || null,
amount: entry.amount
}))
}
},
create: {
purchaseId,
status: payload.status,
weightBuy: toDecimal(payload.weight_buy),
weightReceived: toDecimal(payload.weight_received),
weightFinal: toDecimal(payload.weight_final),
moistureBuyPercent: toDecimal(payload.moisture_buy_percent),
moistureReceivedPercent: toDecimal(payload.moisture_received_percent),
moistureFinalPercent: toDecimal(payload.moisture_final_percent),
aboveAverageRatioPercent: toDecimal(payload.above_average_ratio_percent),
averagePrice: toDecimal(payload.average_price),
modalBeli: toDecimal(payload.modal_beli),
modalMasuk: toDecimal(payload.modal_masuk),
modalJual: toDecimal(payload.modal_jual),
modalBarang: toDecimal(payload.modal_barang),
totalModalBeli: toDecimal(payload.total_modal_beli),
totalModalMal: toDecimal(payload.total_modal_mal),
marketReferencePrice: toDecimal(payload.market_reference_price),
marketValuationTotal: toDecimal(payload.market_valuation_total),
agentProfitShareTotal: payload.agent_profit_share_total,
notes: payload.notes || null,
costEntries: {
create: payload.cost_entries.map((entry) => ({
costType: entry.cost_type,
description: entry.description || null,
amount: entry.amount
}))
}
}
});
}
export async function GET(request: Request, context: RouteContext) {
const auth = requireApiAccess(request);
if (!auth.ok) return auth.response;
const parsedId = parseId((await context.params).purchaseId);
if (parsedId === null) {
return NextResponse.json({ message: "Invalid purchase id" }, { status: 400 });
}
try {
const purchase = await prisma.purchase.findUnique({
where: { id: parsedId },
select: {
id: true,
purchaseNo: true,
purchaseDate: true,
status: true,
moistureBuyPercent: true,
moistureReceivedPercent: true,
aboveAverageRatioPercent: true,
mkSharePercent: true,
nonMkSharePercent: true,
shippingCost: true,
incomingOperationalCost: true,
afterArrivalOperationalCost: true,
agent: analysisInclude.agent,
profitShareScheme: analysisInclude.profitShareScheme,
lines: analysisInclude.lines,
lots: analysisInclude.lots,
analysis: analysisInclude.analysis
}
});
if (!purchase) {
return NextResponse.json({ message: "Purchase not found" }, { status: 404 });
}
if (purchase.status !== "SUBMITTED") {
return NextResponse.json(
{ message: "Purchase analysis hanya tersedia untuk purchase final" },
{ status: 404 }
);
}
return NextResponse.json({
data: serializePurchaseAnalysisDetail(purchase)
});
} catch (error) {
console.error(`Failed to load purchase analysis detail for ${parsedId.toString()}:`, error);
return NextResponse.json(
{ message: "Gagal memuat detail analisis pembelian" },
{ status: 500 }
);
}
}
export async function PUT(request: Request, context: RouteContext) {
const auth = requireApiAccess(request);
if (!auth.ok) return auth.response;
const parsedId = parseId((await context.params).purchaseId);
if (parsedId === null) {
return NextResponse.json({ message: "Invalid purchase id" }, { status: 400 });
}
let requestPayload: unknown;
try {
requestPayload = await request.json();
} catch {
return NextResponse.json({ message: "Invalid request body" }, { status: 400 });
}
const parsed = purchaseAnalysisInputSchema.safeParse(requestPayload);
if (!parsed.success) {
return NextResponse.json(
{
message: "Validasi gagal",
errors: parsed.error.flatten().fieldErrors
},
{ status: 400 }
);
}
try {
const purchase = await prisma.purchase.findUnique({
where: { id: parsedId },
select: { id: true }
});
if (!purchase) {
return NextResponse.json({ message: "Purchase not found" }, { status: 404 });
}
const existingAnalysis = await prisma.purchaseAnalysis.findUnique({
where: { purchaseId: parsedId },
include: { costEntries: true }
});
await upsertAnalysis(parsedId, parsed.data);
const refreshed = await prisma.purchase.findUnique({
where: { id: parsedId },
select: {
id: true,
purchaseNo: true,
purchaseDate: true,
status: true,
moistureBuyPercent: true,
moistureReceivedPercent: true,
aboveAverageRatioPercent: true,
mkSharePercent: true,
nonMkSharePercent: true,
shippingCost: true,
incomingOperationalCost: true,
afterArrivalOperationalCost: true,
agent: analysisInclude.agent,
profitShareScheme: analysisInclude.profitShareScheme,
lines: analysisInclude.lines,
lots: analysisInclude.lots,
analysis: analysisInclude.analysis
}
});
if (!refreshed) {
return NextResponse.json({ message: "Purchase not found" }, { status: 404 });
}
if (refreshed.status !== "SUBMITTED") {
return NextResponse.json(
{ message: "Purchase analysis hanya tersedia untuk purchase final" },
{ status: 404 }
);
}
await createAuditTrailSafe({
userId: auth.user.id,
action: "PURCHASE_ANALYSIS_SAVED",
entityType: "PURCHASE_ANALYSIS",
entityId: parsedId,
method: request.method,
pathname: new URL(request.url).pathname,
statusCode: 200,
summary: `Purchase analysis untuk purchase ${parsedId.toString()} disimpan`,
metadata: buildAuditChangeMetadata(
{
status: existingAnalysis?.status ?? null,
weight_buy: existingAnalysis?.weightBuy?.toNumber() ?? null,
weight_received: existingAnalysis?.weightReceived?.toNumber() ?? null,
weight_final: existingAnalysis?.weightFinal?.toNumber() ?? null,
moisture_buy_percent: existingAnalysis?.moistureBuyPercent?.toNumber() ?? null,
moisture_received_percent: existingAnalysis?.moistureReceivedPercent?.toNumber() ?? null,
moisture_final_percent: existingAnalysis?.moistureFinalPercent?.toNumber() ?? null,
above_average_ratio_percent: existingAnalysis?.aboveAverageRatioPercent?.toNumber() ?? null,
average_price: existingAnalysis?.averagePrice?.toNumber() ?? null,
modal_beli: existingAnalysis?.modalBeli?.toNumber() ?? null,
modal_masuk: existingAnalysis?.modalMasuk?.toNumber() ?? null,
modal_jual: existingAnalysis?.modalJual?.toNumber() ?? null,
modal_barang: existingAnalysis?.modalBarang?.toNumber() ?? null,
total_modal_beli: existingAnalysis?.totalModalBeli?.toNumber() ?? null,
total_modal_mal: existingAnalysis?.totalModalMal?.toNumber() ?? null,
market_reference_price: existingAnalysis?.marketReferencePrice?.toNumber() ?? null,
market_valuation_total: existingAnalysis?.marketValuationTotal?.toNumber() ?? null,
agent_profit_share_total: existingAnalysis?.agentProfitShareTotal?.toNumber() ?? null,
notes: existingAnalysis?.notes ?? null,
cost_entries: (existingAnalysis?.costEntries ?? []).map((entry) => ({
cost_type: entry.costType,
description: entry.description,
amount: entry.amount.toNumber()
}))
},
{
status: parsed.data.status,
weight_buy: parsed.data.weight_buy,
weight_received: parsed.data.weight_received,
weight_final: parsed.data.weight_final,
moisture_buy_percent: parsed.data.moisture_buy_percent,
moisture_received_percent: parsed.data.moisture_received_percent,
moisture_final_percent: parsed.data.moisture_final_percent,
above_average_ratio_percent: parsed.data.above_average_ratio_percent,
average_price: parsed.data.average_price,
modal_beli: parsed.data.modal_beli,
modal_masuk: parsed.data.modal_masuk,
modal_jual: parsed.data.modal_jual,
modal_barang: parsed.data.modal_barang,
total_modal_beli: parsed.data.total_modal_beli,
total_modal_mal: parsed.data.total_modal_mal,
market_reference_price: parsed.data.market_reference_price,
market_valuation_total: parsed.data.market_valuation_total,
agent_profit_share_total: parsed.data.agent_profit_share_total,
notes: parsed.data.notes || null,
cost_entries: parsed.data.cost_entries
}
)
});
return NextResponse.json({
data: serializePurchaseAnalysisDetail(refreshed)
});
} catch (error) {
console.error(`Failed to save purchase analysis for ${parsedId.toString()}:`, error);
return NextResponse.json(
{ message: "Gagal menyimpan analisis pembelian" },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,98 @@
import { NextResponse } from "next/server";
import { requireApiAccess } from "@/lib/authorization";
import { prisma } from "@/lib/prisma";
import { serializePurchaseAnalysisListItem } from "@/features/purchase-analysis/lib/serialize-purchase-analysis";
const analysisInclude = {
agent: {
select: {
id: true,
name: true
}
},
profitShareScheme: {
select: {
shareAgent: true
}
},
lines: {
select: {
subtotal: true,
qtyOrdered: true,
qtyReceived: true,
qtyAccepted: true,
unitPrice: true,
malUnitPrice: true,
moistureReceivedPercent: true,
purchaseMoisturePercent: true,
marketReferencePrice: true,
unit: {
select: {
code: true
}
}
}
},
lots: {
select: {
originalQty: true,
finalMoisturePercent: true,
aboveAverageRatioPercent: true,
unit: {
select: {
code: true
}
}
}
},
analysis: {
include: {
costEntries: true
}
}
} as const;
export async function GET(request: Request) {
const auth = requireApiAccess(request);
if (!auth.ok) return auth.response;
try {
const purchases = await prisma.purchase.findMany({
where: {
status: "SUBMITTED",
purchaseType: "REGULAR"
},
select: {
id: true,
purchaseNo: true,
purchaseDate: true,
status: true,
moistureBuyPercent: true,
moistureReceivedPercent: true,
aboveAverageRatioPercent: true,
mkSharePercent: true,
nonMkSharePercent: true,
shippingCost: true,
incomingOperationalCost: true,
afterArrivalOperationalCost: true,
agent: analysisInclude.agent,
profitShareScheme: analysisInclude.profitShareScheme,
lines: analysisInclude.lines,
lots: analysisInclude.lots,
analysis: analysisInclude.analysis
},
orderBy: [{ createdAt: "desc" }]
});
return NextResponse.json({
data: purchases.map(serializePurchaseAnalysisListItem)
});
} catch (error) {
console.error("Failed to load purchase analyses:", error);
return NextResponse.json(
{ message: "Gagal memuat analisis pembelian" },
{ status: 500 }
);
}
}