diff --git a/routes/collectorPortal.js b/routes/collectorPortal.js index 9df579a..27a6fb3 100644 --- a/routes/collectorPortal.js +++ b/routes/collectorPortal.js @@ -177,16 +177,10 @@ router.get('/', requireCollectorSession, (req, res) => { const todayDay = now.getDate(); const collectorId = Number(req.session.collectorId || 0); - const collectorObj = db.prepare('SELECT area FROM collectors WHERE id = ?').get(collectorId); - const collectorArea = String(collectorObj?.area || req.session.collectorArea || '').trim(); - req.session.collectorArea = collectorArea; + const collectorObj = db.prepare('SELECT id, area FROM collectors WHERE id = ?').get(collectorId); + req.session.collectorArea = String(collectorObj?.area || req.session.collectorArea || '').trim(); - let collectorWhere = '(c.collector_id = ? OR c.collector_id IS NULL)'; - let collectorParams = [collectorId]; - if (collectorArea) { - collectorWhere = '(c.collector_id = ? OR ((c.collector_id IS NULL OR c.collector_id = 0) AND LOWER(TRIM(c.area)) = LOWER(TRIM(?))))'; - collectorParams = [collectorId, collectorArea]; - } + const { where: collectorWhere, params: collectorParams } = customerSvc.getCollectorCustomerWhere(collectorObj, 'c'); let q = ` SELECT c.id as customer_id, @@ -351,6 +345,14 @@ router.post('/payment-request', requireCollectorSession, express.urlencoded({ ex if (!inv) throw new Error('Tagihan tidak ditemukan'); if (String(inv.status || '').toLowerCase() === 'paid') throw new Error('Tagihan sudah lunas'); + { + const collectorIdCheck = Number(req.session.collectorId || 0); + const invCust = customerSvc.getCustomerById(inv.customer_id); + if (!customerSvc.canCollectorAccessCustomer(collectorIdCheck, invCust)) { + throw new Error('Tagihan ini di luar wilayah/tugas Anda'); + } + } + const existingPending = db.prepare(` SELECT id FROM collector_payment_requests WHERE invoice_id = ? AND status = 'pending' @@ -472,6 +474,9 @@ router.get('/invoice/:id/print-thermal', requireCollectorSession, (req, res) => const customer = customerSvc.getCustomerById(invoice.customer_id); if (!customer) return res.status(404).send('Data pelanggan tidak ditemukan'); + if (!customerSvc.canCollectorAccessCustomer(Number(req.session.collectorId || 0), customer)) { + return res.status(403).send('Invoice di luar wilayah tugas Anda'); + } const settings = getSettings(); res.render('collector/print_thermal', { @@ -498,6 +503,9 @@ router.post('/invoice/:id/send-pdf-wa', requireCollectorSession, async (req, res const customer = customerSvc.getCustomerById(inv.customer_id); if (!customer || !customer.phone) return res.json({ success: false, message: 'Nomor HP pelanggan tidak valid' }); + if (!customerSvc.canCollectorAccessCustomer(Number(req.session.collectorId || 0), customer)) { + return res.json({ success: false, message: 'Invoice di luar wilayah tugas Anda' }); + } const settings = getSettings(); const pdfBuffer = await pdfSvc.generateInvoicePdfBuffer(inv, customer, settings); diff --git a/routes/customerAPI.js b/routes/customerAPI.js index 7cdb3fe..65dd55e 100644 --- a/routes/customerAPI.js +++ b/routes/customerAPI.js @@ -740,6 +740,13 @@ router.get('/app/collector/dashboard', requireCollectorApiAuth, (req, res) => { const curMonth = now.getMonth() + 1; const curYear = now.getFullYear(); + const isFullAdmin = req.collector?.role === 'admin' || req.collector?.role === 'root'; + const collectorId = Number(req.collector?.collectorId || req.collector?.id || 0); + const collector = collectorId + ? db.prepare('SELECT id, name, area, auto_approve FROM collectors WHERE id = ?').get(collectorId) + : null; + const colFilter = (!isFullAdmin && collector) ? customerSvc.getCollectorCustomerWhere(collector, 'c') : null; + let q = ` SELECT c.id, c.name, c.phone, c.address, c.status, c.area, c.isolate_day, c.pppoe_username, p.name as package_name, p.price as package_price, @@ -752,6 +759,11 @@ router.get('/app/collector/dashboard', requireCollectorApiAuth, (req, res) => { `; const params = [curMonth, curYear]; + if (colFilter) { + q += ` AND ${colFilter.where}`; + params.push(...colFilter.params); + } + if (filterStatus === 'unpaid') { q += ` AND (i.status = 'unpaid' OR i.status IS NULL)`; } else if (filterStatus === 'today') { @@ -770,6 +782,7 @@ router.get('/app/collector/dashboard', requireCollectorApiAuth, (req, res) => { q += ` ORDER BY (CASE WHEN c.status = 'suspended' THEN 0 WHEN i.status = 'unpaid' THEN 1 ELSE 2 END), c.name ASC LIMIT 150`; const list = db.prepare(q).all(...params) || []; + const statsWhere = colFilter ? ` WHERE ${colFilter.where}` : ''; const stats = db.prepare(` SELECT SUM(CASE WHEN (i.status = 'unpaid' OR i.status IS NULL) THEN 1 ELSE 0 END) as unpaid_count, @@ -779,7 +792,8 @@ router.get('/app/collector/dashboard', requireCollectorApiAuth, (req, res) => { FROM customers c LEFT JOIN packages p ON p.id = c.package_id LEFT JOIN invoices i ON i.customer_id = c.id AND i.period_month = ? AND i.period_year = ? - `).get(todayDay, curMonth, curYear) || {}; + ${statsWhere} + `).get(todayDay, curMonth, curYear, ...(colFilter ? colFilter.params : [])) || {}; res.json({ success: true, @@ -819,7 +833,17 @@ router.post('/app/collector/pay-bill', requireCollectorApiAuth, async (req, res) if (!inv) return res.status(404).json({ success: false, message: 'Tagihan tidak ditemukan' }); const collectorId = Number(req.collector?.collectorId || req.collector?.id || 0); - const collector = collectorId ? db.prepare('SELECT id, name, auto_approve FROM collectors WHERE id = ?').get(collectorId) : null; + const collector = collectorId ? db.prepare('SELECT id, name, area, auto_approve FROM collectors WHERE id = ?').get(collectorId) : null; + + const isFullAdmin = req.collector?.role === 'admin' || req.collector?.role === 'root'; + if (!isFullAdmin && collector) { + const cust = customerSvc.getCustomerById(inv.customer_id); + if (!cust) return res.status(404).json({ success: false, message: 'Pelanggan tidak ditemukan' }); + if (!customerSvc.canCollectorAccessCustomer(collector, cust)) { + return res.status(403).json({ success: false, message: 'Akses ditolak: pelanggan di luar wilayah/tugas Anda' }); + } + } + const collectorName = collector?.name || req.collector?.name || 'Kolektor Lapangan'; const isAutoApprove = collector && collector.auto_approve === 1; diff --git a/routes/techPortal.js b/routes/techPortal.js index d471492..83a175d 100644 --- a/routes/techPortal.js +++ b/routes/techPortal.js @@ -176,7 +176,7 @@ router.get('/', requireTechSession, (req, res) => { // --- OPEN TICKETS (Pool) --- router.get('/pool', requireTechSession, (req, res) => { - const openTickets = techSvc.getOpenTickets(); + const openTickets = techSvc.getOpenTickets(req.session.techId); res.render('tech/pool', { title: 'Tiket Baru', company: company(), @@ -201,8 +201,15 @@ router.get('/history', requireTechSession, (req, res) => { // --- NETWORK MAP --- router.get('/map', requireTechSession, (req, res) => { - const customers = customerSvc.getAllCustomers(); - const odps = odpSvc.getAllOdps(); + const tech = techSvc.getTechById(req.session.techId); + let customers = customerSvc.getAllCustomers(); + let odps = odpSvc.getAllOdps(); + + const filter = tech ? techSvc.getTechAreaWhere(tech, 'c') : null; + if (filter && filter.isRestricted) { + customers = customers.filter(c => filter.areas.includes(String(c.area || '').trim().toLowerCase())); + odps = odps.filter(o => !o.area || filter.areas.includes(String(o.area || '').trim().toLowerCase())); + } res.render('tech/map', { title: 'Peta Jaringan', @@ -346,7 +353,12 @@ router.get('/monitoring', requireTechSession, async (req, res) => { // --- CREATE CUSTOMER (Technician) --- router.get('/customers/new', requireTechSession, (req, res) => { const packages = customerSvc.getAllPackages(); - const odps = odpSvc.getAllOdps(); + let odps = odpSvc.getAllOdps(); + const tech = techSvc.getTechById(req.session.techId); + const filter = tech ? techSvc.getTechAreaWhere(tech, 'c') : null; + if (filter && filter.isRestricted) { + odps = odps.filter(o => !o.area || filter.areas.includes(String(o.area || '').trim().toLowerCase())); + } const routers = mikrotikService.getAllRouters(); const olts = oltSvc.getAllOlts(); res.render('tech/create_customer', { @@ -366,8 +378,20 @@ router.post('/customers', requireTechSession, express.urlencoded({ extended: tru const name = String(req.body.name || '').trim(); if (!name) throw new Error('Nama pelanggan wajib diisi'); + const tech = techSvc.getTechById(req.session.techId); + const areaFilter = tech ? techSvc.getTechAreaWhere(tech, 'c') : null; + let areaInput = String(req.body.area || '').trim(); + if (areaFilter && areaFilter.isRestricted) { + if (!areaInput) { + areaInput = areaFilter.areas[0]; + } else if (!areaFilter.areas.includes(areaInput.toLowerCase())) { + throw new Error(`Anda hanya dapat mendaftarkan pelanggan di wilayah tugas Anda (${tech.area})`); + } + } + const customerData = { name, + area: areaInput, phone: String(req.body.phone || '').trim(), email: String(req.body.email || '').trim(), nik: String(req.body.nik || '').trim(), @@ -461,6 +485,16 @@ router.post('/customers', requireTechSession, express.urlencoded({ extended: tru // API Endpoints for Technician const customerDevice = require('../services/customerDeviceService'); +// Teknisi hanya boleh menyentuh perangkat pelanggan di wilayahnya. +function checkTechTagAccess(req, tag) { + const tech = req.session?.techId ? techSvc.getTechById(req.session.techId) : null; + const filter = tech ? techSvc.getTechAreaWhere(tech, 'c') : null; + if (!filter || !filter.isRestricted) return true; + const cust = customerSvc.findCustomerByAny(tag); + if (!cust) return true; + return filter.areas.includes(String(cust.area || '').trim().toLowerCase()); +} + router.get('/api/mikrotik/pppoe-users', requireTechSession, async (req, res) => { try { const routerId = req.query.routerId ? Number(req.query.routerId) : null; @@ -499,10 +533,15 @@ router.get('/api/odps/:id/ports', requireTechSession, (req, res) => { router.get('/api/devices', requireTechSession, async (req, res) => { try { const { search, status, acs, limit = 999999, offset = 0 } = req.query; - const customers = db.prepare('SELECT id, name, phone, pppoe_username, genieacs_tag FROM customers').all(); + const tech = techSvc.getTechById(req.session.techId); + const areaFilter = tech ? techSvc.getTechAreaWhere(tech, 'c') : null; + const customers = db.prepare('SELECT id, name, phone, pppoe_username, genieacs_tag, area FROM customers').all(); + const allowedCustomerIds = new Set(); const byPppoe = new Map(); const byTag = new Map(); for (const c of customers) { + if (areaFilter && areaFilter.isRestricted && !areaFilter.areas.includes(String(c.area || '').trim().toLowerCase())) continue; + allowedCustomerIds.add(c.id); const pu = String(c.pppoe_username || '').trim().toLowerCase(); const tg = String(c.genieacs_tag || '').trim(); if (pu) byPppoe.set(pu, c); @@ -549,6 +588,10 @@ router.get('/api/devices', requireTechSession, async (req, res) => { }; }); + if (areaFilter && areaFilter.isRestricted) { + devices = devices.filter(d => d.customerId && allowedCustomerIds.has(d.customerId)); + } + if (search) { const s = search.toLowerCase(); devices = devices.filter(d => @@ -575,6 +618,7 @@ router.get('/api/devices', requireTechSession, async (req, res) => { router.get('/api/device/:tag', requireTechSession, async (req, res) => { try { + if (!checkTechTagAccess(req, req.params.tag)) return res.status(403).json({ error: 'Perangkat di luar wilayah tugas Anda' }); const data = await customerDevice.getCustomerDeviceData(req.params.tag); if (!data || data.status === 'Tidak ditemukan') return res.status(404).json({ error: 'Device not found' }); res.json(data); @@ -584,6 +628,7 @@ router.get('/api/device/:tag', requireTechSession, async (req, res) => { }); router.post('/api/device/:tag/ssid', requireTechSession, express.json(), async (req, res) => { + if (!checkTechTagAccess(req, req.params.tag)) return res.status(403).json({ error: 'Perangkat di luar wilayah tugas Anda' }); const { ssid } = req.body; if (!ssid) return res.status(400).json({ error: 'SSID required' }); const ok = await customerDevice.updateSSID(req.params.tag, ssid); @@ -616,6 +661,7 @@ router.post('/api/device/:tag/ssid', requireTechSession, express.json(), async ( }); router.post('/api/device/:tag/password', requireTechSession, express.json(), async (req, res) => { + if (!checkTechTagAccess(req, req.params.tag)) return res.status(403).json({ error: 'Perangkat di luar wilayah tugas Anda' }); const { password } = req.body; if (!password || password.length < 8) return res.status(400).json({ error: 'Password minimal 8 karakter' }); const ok = await customerDevice.updatePassword(req.params.tag, password); @@ -648,6 +694,7 @@ router.post('/api/device/:tag/password', requireTechSession, express.json(), asy }); router.post('/api/device/:tag/reboot', requireTechSession, async (req, res) => { + if (!checkTechTagAccess(req, req.params.tag)) return res.status(403).json({ error: 'Perangkat di luar wilayah tugas Anda' }); const result = await customerDevice.requestReboot(req.params.tag); res.json(result); }); diff --git a/services/customerService.js b/services/customerService.js index 8b5bc90..539eef2 100644 --- a/services/customerService.js +++ b/services/customerService.js @@ -71,6 +71,59 @@ function calculateExpiredAt(startDateStr, durationDays = 30) { return `${y}-${m}-${day} 23:59:59`; } +/** + * Batas wilayah tugas kolektor (dukung multi-area dipisah koma). + * area kosong / "semua" / "all" = tanpa batas wilayah. + */ +function getCollectorCustomerWhere(collector, tableAlias = 'c') { + let col = collector; + if (typeof collector === 'number' || typeof collector === 'string') { + col = db.prepare('SELECT id, area FROM collectors WHERE id = ?').get(Number(collector)); + } + const colId = Number(col?.id || 0); + const areas = String(col?.area || '') + .split(',') + .map(a => a.trim().toLowerCase()) + .filter(a => a.length > 0 && a !== 'semua' && a !== 'all'); + + if (areas.length > 0) { + const placeholders = areas.map(() => '?').join(', '); + return { + where: `(LOWER(TRIM(${tableAlias}.area)) IN (${placeholders}) AND (${tableAlias}.collector_id = ? OR ${tableAlias}.collector_id IS NULL OR ${tableAlias}.collector_id = 0))`, + params: [...areas, colId], + isRestricted: true, + areas, + }; + } + return { + where: `(${tableAlias}.collector_id = ? OR ${tableAlias}.collector_id IS NULL OR ${tableAlias}.collector_id = 0)`, + params: [colId], + isRestricted: false, + areas: [], + }; +} + +function canCollectorAccessCustomer(collector, customer) { + if (!collector || !customer) return false; + let col = collector; + if (typeof collector === 'number' || typeof collector === 'string') { + col = db.prepare('SELECT id, area FROM collectors WHERE id = ?').get(Number(collector)); + } + const colId = Number(col?.id || 0); + const custColId = Number(customer?.collector_id || 0); + if (custColId > 0 && custColId !== colId) return false; + + const areas = String(col?.area || '') + .split(',') + .map(a => a.trim().toLowerCase()) + .filter(a => a.length > 0 && a !== 'semua' && a !== 'all'); + if (areas.length > 0) { + const custArea = String(customer?.area || '').trim().toLowerCase(); + return !!custArea && areas.includes(custArea); + } + return true; +} + function getAllCustomers(search = '', routerId = null, filterStatus = '', filterArea = '', opts = {}) { const now = getCurrentDateInTimezone(); const month = now.getMonth() + 1; @@ -955,7 +1008,7 @@ Dihapus: ${removed.join(', ') || '-'}`); } module.exports = { - getAllCustomers, getAllCustomerAreas, getCustomerById, createCustomer, updateCustomer, deleteCustomer, bulkDeleteCustomers, getCustomerStats, + getAllCustomers, getAllCustomerAreas, getCollectorCustomerWhere, canCollectorAccessCustomer, getCustomerById, createCustomer, updateCustomer, deleteCustomer, bulkDeleteCustomers, getCustomerStats, getAllPackages, getPackageById, createPackage, updatePackage, deletePackage, suspendCustomer, activateCustomer, autoDeactivateOverdueCustomers, generatePppoeAccount, findCustomerByAny, findCustomerForLogin, updateCustomerCablePath, resetPromoCyclesUsed, getEffectiveRouterId diff --git a/services/techService.js b/services/techService.js index ec7e9c9..0f25800 100644 --- a/services/techService.js +++ b/services/techService.js @@ -9,20 +9,56 @@ function getTechById(id) { return db.prepare('SELECT id, username, name, phone, area FROM technicians WHERE id = ?').get(id); } +/** + * Batas wilayah tugas teknisi. + * area kosong / "semua" / "all" = tanpa batas (semua wilayah). + */ +function getTechAreaWhere(technician, tableAlias = 'c') { + let tech = technician; + if (typeof technician === 'number' || typeof technician === 'string') { + tech = db.prepare('SELECT id, area FROM technicians WHERE id = ?').get(Number(technician)); + } + const areas = String(tech?.area || '') + .split(',') + .map(a => a.trim().toLowerCase()) + .filter(a => a.length > 0 && a !== 'semua' && a !== 'all'); + + if (areas.length > 0) { + const placeholders = areas.map(() => '?').join(', '); + return { + where: `LOWER(TRIM(${tableAlias}.area)) IN (${placeholders})`, + params: areas, + isRestricted: true, + areas, + }; + } + return { where: '1=1', params: [], isRestricted: false, areas: [] }; +} + +function canTechAccessCustomer(technician, customer) { + if (!technician) return false; + const filter = getTechAreaWhere(technician); + if (!filter.isRestricted) return true; + const custArea = String(customer?.area || '').trim().toLowerCase(); + return !!custArea && filter.areas.includes(custArea); +} + function getTechStats(techId) { const total = db.prepare("SELECT COUNT(*) as count FROM tickets WHERE technician_id = ?").get(techId).count; const inProgress = db.prepare("SELECT COUNT(*) as count FROM tickets WHERE technician_id = ? AND status = 'in_progress'").get(techId).count; const resolved = db.prepare("SELECT COUNT(*) as count FROM tickets WHERE technician_id = ? AND status = 'resolved'").get(techId).count; - - // Ambil open tickets yang belum ada teknisinya atau di-assign ke teknisi ini - const open = db.prepare("SELECT COUNT(*) as count FROM tickets WHERE status = 'open'").get().count; + + const filter = techId ? getTechAreaWhere(techId, 'c') : null; + const open = filter && filter.isRestricted + ? db.prepare(`SELECT COUNT(*) as count FROM tickets t JOIN customers c ON t.customer_id = c.id WHERE t.status = 'open' AND ${filter.where}`).get(...filter.params).count + : db.prepare("SELECT COUNT(*) as count FROM tickets WHERE status = 'open'").get().count; return { total, open, inProgress, resolved }; } function getAssignedTickets(techId) { return db.prepare(` - SELECT t.*, c.name as customer_name, c.phone as customer_phone, c.address as customer_address + SELECT t.*, c.name as customer_name, c.phone as customer_phone, c.address as customer_address, c.area as customer_area FROM tickets t JOIN customers c ON t.customer_id = c.id WHERE t.technician_id = ? AND t.status != 'resolved' @@ -30,19 +66,21 @@ function getAssignedTickets(techId) { `).all(techId); } -function getOpenTickets() { +function getOpenTickets(techId = null) { + const filter = techId ? getTechAreaWhere(techId, 'c') : null; + const areaWhere = filter && filter.isRestricted ? `AND ${filter.where}` : ''; return db.prepare(` - SELECT t.*, c.name as customer_name, c.phone as customer_phone, c.address as customer_address + SELECT t.*, c.name as customer_name, c.phone as customer_phone, c.address as customer_address, c.area as customer_area FROM tickets t JOIN customers c ON t.customer_id = c.id - WHERE t.status = 'open' + WHERE t.status = 'open' ${areaWhere} ORDER BY t.created_at DESC - `).all(); + `).all(...(filter && filter.isRestricted ? filter.params : [])); } function getResolvedTickets(techId) { return db.prepare(` - SELECT t.*, c.name as customer_name, c.phone as customer_phone, c.address as customer_address + SELECT t.*, c.name as customer_name, c.phone as customer_phone, c.address as customer_address, c.area as customer_area FROM tickets t JOIN customers c ON t.customer_id = c.id WHERE t.technician_id = ? AND t.status = 'resolved' @@ -52,11 +90,27 @@ function getResolvedTickets(techId) { function takeTicket(ticketId, techId) { let validTechId = Number(techId); - const exists = db.prepare('SELECT id FROM technicians WHERE id = ?').get(validTechId); - if (!exists) { - const fallback = db.prepare('SELECT id FROM technicians WHERE is_active = 1 ORDER BY id ASC LIMIT 1').get(); - if (fallback) validTechId = fallback.id; + let tech = db.prepare('SELECT id, area FROM technicians WHERE id = ?').get(validTechId); + if (!tech) { + const fallback = db.prepare('SELECT id, area FROM technicians WHERE is_active = 1 ORDER BY id ASC LIMIT 1').get(); + if (!fallback) throw new Error('Teknisi tidak ditemukan.'); + validTechId = fallback.id; + tech = fallback; } + + const ticket = db.prepare(` + SELECT t.id, t.customer_id, t.status, c.area as customer_area + FROM tickets t + LEFT JOIN customers c ON t.customer_id = c.id + WHERE t.id = ? + `).get(ticketId); + + if (!ticket) throw new Error('Tiket tidak ditemukan.'); + if (ticket.status !== 'open') throw new Error('Tiket sudah diambil oleh teknisi lain atau sudah selesai.'); + if (ticket.customer_id && !canTechAccessCustomer(tech, { area: ticket.customer_area })) { + throw new Error(`Tiket ini berada di wilayah "${ticket.customer_area || 'Tanpa Area'}", di luar wilayah tugas Anda (${tech.area}).`); + } + db.prepare("UPDATE tickets SET technician_id = ?, status = 'in_progress', updated_at = (NOW_LOCAL()) WHERE id = ?").run(validTechId, ticketId); return validTechId; } @@ -102,6 +156,8 @@ function createTechnician(data) { module.exports = { authenticate, getTechById, + getTechAreaWhere, + canTechAccessCustomer, getTechStats, getAssignedTickets, getOpenTickets, diff --git a/tests/unit/area-access.test.js b/tests/unit/area-access.test.js new file mode 100644 index 0000000..8c58af2 --- /dev/null +++ b/tests/unit/area-access.test.js @@ -0,0 +1,72 @@ +/** + * Self-check: pembatasan wilayah teknisi & kolektor. + * Jalankan: node tests/unit/area-access.test.js + * DB dan modul berat di-stub supaya bisa jalan tanpa node_modules. + */ +const assert = require('assert'); +const path = require('path'); + +function stub(rel, exports) { + const p = require.resolve(path.join(__dirname, '..', '..', rel)); + require.cache[p] = { id: p, filename: p, loaded: true, exports }; +} + +const techRows = { 1: { id: 1, area: 'Area A, Area B' }, 2: { id: 2, area: '' }, 3: { id: 3, area: 'semua' } }; +const colRows = { 7: { id: 7, area: 'Area A' }, 8: { id: 8, area: '' } }; + +const db = { + prepare(sql) { + return { + get(id) { + if (/FROM technicians/.test(sql)) return techRows[Number(id)]; + if (/FROM collectors/.test(sql)) return colRows[Number(id)]; + return undefined; + }, + all() { return []; }, + run() { return { changes: 1 }; }, + }; + }, + exec() {}, +}; + +stub('config/database', db); +stub('config/logger', { logger: { info() {}, warn() {}, error() {}, debug() {} } }); +stub('config/settingsManager', { getCurrentDateInTimezone: () => new Date(), getSetting: () => '' }); +stub('services/customerIdService', { generateCustomerCode: () => 'C0001', backfillCustomerCodes: () => 0 }); + +const tech = require('../../services/techService'); +const cust = require('../../services/customerService'); + +// Teknisi: area dipisah koma -> dibatasi ke dua area +const tf = tech.getTechAreaWhere(1, 'c'); +assert.strictEqual(tf.isRestricted, true); +assert.deepStrictEqual(tf.areas, ['area a', 'area b']); +assert.ok(tf.where.includes('IN (?, ?)')); + +// Teknisi tanpa area / "semua" -> tidak dibatasi +assert.strictEqual(tech.getTechAreaWhere(2).isRestricted, false); +assert.strictEqual(tech.getTechAreaWhere(3).isRestricted, false); + +// canTechAccessCustomer +assert.strictEqual(tech.canTechAccessCustomer(1, { area: 'area b' }), true); +assert.strictEqual(tech.canTechAccessCustomer(1, { area: 'area z' }), false); +assert.strictEqual(tech.canTechAccessCustomer(1, { area: '' }), false); +assert.strictEqual(tech.canTechAccessCustomer(2, { area: 'apa saja' }), true); +assert.strictEqual(tech.canTechAccessCustomer(null, { area: 'area a' }), false); + +// Kolektor: pelanggan yang ditugaskan ke kolektor lain selalu ditolak +assert.strictEqual(cust.canCollectorAccessCustomer(7, { area: 'area a', collector_id: 0 }), true); +assert.strictEqual(cust.canCollectorAccessCustomer(7, { area: 'area a', collector_id: 7 }), true); +assert.strictEqual(cust.canCollectorAccessCustomer(7, { area: 'area a', collector_id: 8 }), false); +assert.strictEqual(cust.canCollectorAccessCustomer(7, { area: 'area z', collector_id: 0 }), false); +assert.strictEqual(cust.canCollectorAccessCustomer(8, { area: 'area z', collector_id: 0 }), true); + +// Where kolektor memuat batas area + kolektor_id +const cf = cust.getCollectorCustomerWhere(7, 'c'); +assert.strictEqual(cf.isRestricted, true); +assert.deepStrictEqual(cf.params, ['area a', 7]); +const cf2 = cust.getCollectorCustomerWhere(8, 'c'); +assert.strictEqual(cf2.isRestricted, false); +assert.deepStrictEqual(cf2.params, [8]); + +console.log('OK: area-access (teknisi & kolektor)');