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 } ); } }