Initial import of AbelBirdNest Stock
This commit is contained in:
347
src/app/api/v1/purchase-analyses/[purchaseId]/route.ts
Normal file
347
src/app/api/v1/purchase-analyses/[purchaseId]/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
98
src/app/api/v1/purchase-analyses/route.ts
Normal file
98
src/app/api/v1/purchase-analyses/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user