This commit is contained in:
2026-10-10 11:21:23 +05:30
parent ee71d4c24e
commit 335569f682
5 changed files with 172 additions and 0 deletions
@@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "Part" (
"id" TEXT NOT NULL PRIMARY KEY,
"partNumber" TEXT NOT NULL,
"product" TEXT NOT NULL,
"price" REAL NOT NULL,
"uom" TEXT NOT NULL,
"model" TEXT NOT NULL,
"createdById" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Part_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Part_partNumber_key" ON "Part"("partNumber");
+14
View File
@@ -23,6 +23,7 @@ model User {
transfers StockTransfer[] transfers StockTransfer[]
events Event[] events Event[]
servicePoints ServicePoint[] servicePoints ServicePoint[]
parts Part[]
loginRequests LoginRequest[] @relation("DealerLoginRequests") loginRequests LoginRequest[] @relation("DealerLoginRequests")
loginDecisions LoginRequest[] @relation("LoginDecisions") loginDecisions LoginRequest[] @relation("LoginDecisions")
} }
@@ -141,3 +142,16 @@ model ServicePoint {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model Part {
id String @id @default(cuid())
partNumber String @unique
product String
price Float
uom String
model String
createdById String
createdBy User @relation(fields: [createdById], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
+52
View File
@@ -476,6 +476,58 @@ async function run(port) {
'service point search failed' 'service point search failed'
); );
const partForbidden = await request(port, 'POST', '/api/parts', {
token: dealerToken,
body: {
partNumber: 'CG125-B010',
product: 'CLUTCH HOUSING - CG125',
price: 9800,
uom: 'PCS',
model: 'CG - 125',
},
});
assert(partForbidden.status === 403, 'dealer should not add parts');
const partMissing = await request(port, 'POST', '/api/parts', {
token: repToken,
body: { partNumber: 'CG125-B010', product: 'CLUTCH HOUSING - CG125' },
});
assert(partMissing.status === 400, 'part create should require every field');
const part = await request(port, 'POST', '/api/parts', {
token: repToken,
body: {
partNumber: 'CG125-B010',
product: 'CLUTCH HOUSING - CG125',
price: 9800,
uom: 'PCS',
model: 'CG - 125',
},
});
assert(part.status === 201, 'part create failed');
const duplicatePart = await request(port, 'POST', '/api/parts', {
token: repToken,
body: {
partNumber: 'CG125-B010',
product: 'CLUTCH HOUSING - CG125',
price: 9800,
uom: 'PCS',
model: 'CG - 125',
},
});
assert(duplicatePart.status === 409, 'duplicate part number should be rejected');
const foundPart = await request(port, 'GET', '/api/parts?model=CG%20-%20125&search=CLUTCH', {
token: dealerToken,
});
assert(
foundPart.status === 200 &&
foundPart.json.data.some((item) => item.partNumber === 'CG125-B010' && item.price === 9800) &&
foundPart.json.models.includes('CG - 125'),
'part search failed'
);
const reassigned = await request(port, 'PATCH', '/api/admin/assign-dealer', { const reassigned = await request(port, 'PATCH', '/api/admin/assign-dealer', {
token: adminToken, token: adminToken,
body: { salesRepId: rep.json.data.id, dealerId: dealerC.json.data.id, assigned: true }, body: { salesRepId: rep.json.data.id, dealerId: dealerC.json.data.id, assigned: true },
+2
View File
@@ -8,6 +8,7 @@ const dealerRoutes = require('./routes/dealers');
const stockRoutes = require('./routes/stocks'); const stockRoutes = require('./routes/stocks');
const eventRoutes = require('./routes/events'); const eventRoutes = require('./routes/events');
const servicePointRoutes = require('./routes/servicePoints'); const servicePointRoutes = require('./routes/servicePoints');
const partRoutes = require('./routes/parts');
const adminRoutes = require('./routes/admin'); const adminRoutes = require('./routes/admin');
const app = express(); const app = express();
@@ -24,6 +25,7 @@ app.use('/api/dealers', dealerRoutes);
app.use('/api/stocks', stockRoutes); app.use('/api/stocks', stockRoutes);
app.use('/api/events', eventRoutes); app.use('/api/events', eventRoutes);
app.use('/api/service-points', servicePointRoutes); app.use('/api/service-points', servicePointRoutes);
app.use('/api/parts', partRoutes);
app.use('/api/admin', adminRoutes); app.use('/api/admin', adminRoutes);
app.use((req, res) => { app.use((req, res) => {
+88
View File
@@ -0,0 +1,88 @@
const express = require('express');
const prisma = require('../db');
const { asyncHandler, httpError } = require('../lib/errors');
const { requireAuth, requireRole } = require('../middleware/auth');
const router = express.Router();
function serializePart(part) {
return {
id: part.id,
partNumber: part.partNumber,
product: part.product,
price: part.price,
uom: part.uom,
model: part.model,
createdAt: part.createdAt,
};
}
function requiredText(value) {
return String(value || '').trim();
}
router.use(requireAuth);
router.get(
'/',
asyncHandler(async (req, res) => {
const model = typeof req.query.model === 'string' ? req.query.model.trim() : '';
const search = typeof req.query.search === 'string' ? req.query.search.trim() : '';
const where = {};
if (model) where.model = model;
if (search) {
where.OR = [{ partNumber: { contains: search } }, { product: { contains: search } }];
}
const [parts, modelRows] = await Promise.all([
prisma.part.findMany({
where,
orderBy: { partNumber: 'asc' },
}),
prisma.part.findMany({
distinct: ['model'],
select: { model: true },
orderBy: { model: 'asc' },
}),
]);
return res.json({
success: true,
data: parts.map(serializePart),
models: modelRows.map((row) => row.model),
});
})
);
router.post(
'/',
requireRole('salesRep', 'superAdmin'),
asyncHandler(async (req, res) => {
const partNumber = requiredText(req.body?.partNumber);
const product = requiredText(req.body?.product);
const uom = requiredText(req.body?.uom);
const model = requiredText(req.body?.model);
const rawPrice = req.body?.price;
const price = typeof rawPrice === 'number' ? rawPrice : Number(requiredText(rawPrice));
if (!partNumber || !product || !uom || !model || rawPrice === '' || rawPrice == null) {
throw httpError(400, 'Part number, product, price, UOM, and model are required');
}
if (!Number.isFinite(price) || price < 0) {
throw httpError(400, 'Price must be a non-negative number');
}
const part = await prisma.part.create({
data: {
partNumber,
product,
price,
uom,
model,
createdById: req.user.id,
},
});
return res.status(201).json({ success: true, data: serializePart(part) });
})
);
module.exports = router;