Spaces:
Sleeping
Sleeping
Upload 20 files
Browse files- package.json +1 -0
- src/db/config.js +1 -1
- src/db/migrate.js +6 -0
- src/db/migrate2.js +40 -0
- src/routes/parties.js +7 -7
- src/routes/transactions.js +160 -0
package.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
| 7 |
"start": "node src/server.js",
|
| 8 |
"dev": "nodemon src/server.js",
|
| 9 |
"migrate": "node src/db/migrate.js",
|
|
|
|
| 10 |
"seed": "node src/db/seed.js",
|
| 11 |
"reset": "node src/db/reset.js"
|
| 12 |
},
|
|
|
|
| 7 |
"start": "node src/server.js",
|
| 8 |
"dev": "nodemon src/server.js",
|
| 9 |
"migrate": "node src/db/migrate.js",
|
| 10 |
+
"migrate2": "node src/db/migrate2.js",
|
| 11 |
"seed": "node src/db/seed.js",
|
| 12 |
"reset": "node src/db/reset.js"
|
| 13 |
},
|
src/db/config.js
CHANGED
|
@@ -26,4 +26,4 @@ pool.on('error', (err) => {
|
|
| 26 |
process.exit(-1);
|
| 27 |
});
|
| 28 |
|
| 29 |
-
module.exports = pool;
|
|
|
|
| 26 |
process.exit(-1);
|
| 27 |
});
|
| 28 |
|
| 29 |
+
module.exports = pool;
|
src/db/migrate.js
CHANGED
|
@@ -17,12 +17,18 @@ const createTables = async () => {
|
|
| 17 |
city VARCHAR(100),
|
| 18 |
party_type VARCHAR(20) NOT NULL CHECK (party_type IN ('awaak', 'jawaak', 'both')),
|
| 19 |
current_balance DECIMAL(12, 2) DEFAULT 0,
|
|
|
|
| 20 |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 21 |
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 22 |
)
|
| 23 |
`);
|
| 24 |
console.log('✅ Created table: parties');
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
// 2. Create mirchi_types table
|
| 27 |
await client.query(`
|
| 28 |
CREATE TABLE IF NOT EXISTS mirchi_types (
|
|
|
|
| 17 |
city VARCHAR(100),
|
| 18 |
party_type VARCHAR(20) NOT NULL CHECK (party_type IN ('awaak', 'jawaak', 'both')),
|
| 19 |
current_balance DECIMAL(12, 2) DEFAULT 0,
|
| 20 |
+
past_due DECIMAL(12, 2) DEFAULT 0,
|
| 21 |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 22 |
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 23 |
)
|
| 24 |
`);
|
| 25 |
console.log('✅ Created table: parties');
|
| 26 |
|
| 27 |
+
await client.query(`
|
| 28 |
+
ALTER TABLE parties
|
| 29 |
+
ADD COLUMN IF NOT EXISTS past_due DECIMAL(12, 2) DEFAULT 0
|
| 30 |
+
`);
|
| 31 |
+
|
| 32 |
// 2. Create mirchi_types table
|
| 33 |
await client.query(`
|
| 34 |
CREATE TABLE IF NOT EXISTS mirchi_types (
|
src/db/migrate2.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const pool = require('./config');
|
| 2 |
+
|
| 3 |
+
const migrate2 = async () => {
|
| 4 |
+
const client = await pool.connect();
|
| 5 |
+
|
| 6 |
+
try {
|
| 7 |
+
console.log('🚀 Starting database migration (migrate2)...');
|
| 8 |
+
|
| 9 |
+
await client.query('BEGIN');
|
| 10 |
+
|
| 11 |
+
// Add past_due (opening balance) to parties
|
| 12 |
+
await client.query(`
|
| 13 |
+
ALTER TABLE parties
|
| 14 |
+
ADD COLUMN IF NOT EXISTS past_due DECIMAL(12, 2) DEFAULT 0
|
| 15 |
+
`);
|
| 16 |
+
|
| 17 |
+
// Ensure no NULL values exist
|
| 18 |
+
await client.query(`
|
| 19 |
+
UPDATE parties
|
| 20 |
+
SET past_due = 0
|
| 21 |
+
WHERE past_due IS NULL
|
| 22 |
+
`);
|
| 23 |
+
|
| 24 |
+
await client.query('COMMIT');
|
| 25 |
+
console.log('✅ migrate2 completed successfully!');
|
| 26 |
+
|
| 27 |
+
} catch (error) {
|
| 28 |
+
await client.query('ROLLBACK');
|
| 29 |
+
console.error('❌ migrate2 failed:', error);
|
| 30 |
+
throw error;
|
| 31 |
+
} finally {
|
| 32 |
+
client.release();
|
| 33 |
+
await pool.end();
|
| 34 |
+
}
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
migrate2().catch(err => {
|
| 38 |
+
console.error('Fatal error:', err);
|
| 39 |
+
process.exit(1);
|
| 40 |
+
});
|
src/routes/parties.js
CHANGED
|
@@ -41,7 +41,7 @@ router.post('/', async (req, res) => {
|
|
| 41 |
const client = await pool.connect();
|
| 42 |
|
| 43 |
try {
|
| 44 |
-
const { id, name, phone, city, party_type, current_balance } = req.body;
|
| 45 |
|
| 46 |
if (!name) {
|
| 47 |
return res.status(400).json({ success: false, message: 'Party name is required' });
|
|
@@ -59,18 +59,18 @@ router.post('/', async (req, res) => {
|
|
| 59 |
// Update existing party
|
| 60 |
result = await client.query(
|
| 61 |
`UPDATE parties
|
| 62 |
-
SET name = $1, phone = $2, city = $3, party_type = $4, current_balance = $5, updated_at = CURRENT_TIMESTAMP
|
| 63 |
-
WHERE id = $
|
| 64 |
RETURNING *`,
|
| 65 |
-
[name, phone || '', city || '', party_type || 'both', current_balance || 0, partyId]
|
| 66 |
);
|
| 67 |
} else {
|
| 68 |
// Insert new party
|
| 69 |
result = await client.query(
|
| 70 |
-
`INSERT INTO parties (id, name, phone, city, party_type, current_balance)
|
| 71 |
-
VALUES ($1, $2, $3, $4, $5, $6)
|
| 72 |
RETURNING *`,
|
| 73 |
-
[partyId, name, phone || '', city || '', party_type || 'both', current_balance || 0]
|
| 74 |
);
|
| 75 |
}
|
| 76 |
|
|
|
|
| 41 |
const client = await pool.connect();
|
| 42 |
|
| 43 |
try {
|
| 44 |
+
const { id, name, phone, city, party_type, current_balance, past_due } = req.body;
|
| 45 |
|
| 46 |
if (!name) {
|
| 47 |
return res.status(400).json({ success: false, message: 'Party name is required' });
|
|
|
|
| 59 |
// Update existing party
|
| 60 |
result = await client.query(
|
| 61 |
`UPDATE parties
|
| 62 |
+
SET name = $1, phone = $2, city = $3, party_type = $4, current_balance = $5, past_due = $6, updated_at = CURRENT_TIMESTAMP
|
| 63 |
+
WHERE id = $7
|
| 64 |
RETURNING *`,
|
| 65 |
+
[name, phone || '', city || '', party_type || 'both', current_balance || 0, past_due || 0, partyId]
|
| 66 |
);
|
| 67 |
} else {
|
| 68 |
// Insert new party
|
| 69 |
result = await client.query(
|
| 70 |
+
`INSERT INTO parties (id, name, phone, city, party_type, current_balance, past_due)
|
| 71 |
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
| 72 |
RETURNING *`,
|
| 73 |
+
[partyId, name, phone || '', city || '', party_type || 'both', current_balance || 0, past_due || 0]
|
| 74 |
);
|
| 75 |
}
|
| 76 |
|
src/routes/transactions.js
CHANGED
|
@@ -530,4 +530,164 @@ router.patch('/:id/payment', async (req, res) => {
|
|
| 530 |
}
|
| 531 |
});
|
| 532 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
module.exports = router;
|
|
|
|
| 530 |
}
|
| 531 |
});
|
| 532 |
|
| 533 |
+
// POST revert transaction (rollback stock + balances, then delete)
|
| 534 |
+
router.post('/:id/revert', async (req, res) => {
|
| 535 |
+
const client = await pool.connect();
|
| 536 |
+
|
| 537 |
+
try {
|
| 538 |
+
const { id } = req.params;
|
| 539 |
+
|
| 540 |
+
await client.query('BEGIN');
|
| 541 |
+
|
| 542 |
+
const txResult = await client.query(
|
| 543 |
+
'SELECT * FROM transactions WHERE id = $1',
|
| 544 |
+
[id]
|
| 545 |
+
);
|
| 546 |
+
|
| 547 |
+
if (txResult.rows.length === 0) {
|
| 548 |
+
await client.query('ROLLBACK');
|
| 549 |
+
return res.status(404).json({ success: false, message: 'Transaction not found' });
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
const tx = txResult.rows[0];
|
| 553 |
+
|
| 554 |
+
const itemsResult = await client.query(
|
| 555 |
+
'SELECT * FROM transaction_items WHERE transaction_id = $1',
|
| 556 |
+
[id]
|
| 557 |
+
);
|
| 558 |
+
|
| 559 |
+
const items = itemsResult.rows;
|
| 560 |
+
|
| 561 |
+
if (!items || items.length === 0) {
|
| 562 |
+
await client.query('ROLLBACK');
|
| 563 |
+
return res.status(400).json({ success: false, message: 'No items found for this transaction' });
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
// Rollback lots (inverse of creation logic)
|
| 567 |
+
for (const item of items) {
|
| 568 |
+
if (!item.lot_id) continue;
|
| 569 |
+
|
| 570 |
+
const lotResult = await client.query(
|
| 571 |
+
'SELECT * FROM lots WHERE id = $1',
|
| 572 |
+
[item.lot_id]
|
| 573 |
+
);
|
| 574 |
+
|
| 575 |
+
if (lotResult.rows.length === 0) {
|
| 576 |
+
await client.query('ROLLBACK');
|
| 577 |
+
return res.status(400).json({ success: false, message: `Lot not found for item ${item.id}` });
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
const lot = lotResult.rows[0];
|
| 581 |
+
const netWeight = Number(item.net_weight || 0);
|
| 582 |
+
const grossWeight = Number(item.gross_weight || 0);
|
| 583 |
+
|
| 584 |
+
// AWAAK = Purchase (Stock IN), JAWAAK = Sale (Stock OUT)
|
| 585 |
+
if (tx.bill_type === 'awaak' && !tx.is_return) {
|
| 586 |
+
// Reverting a purchase: subtract stock that was added
|
| 587 |
+
if (Number(lot.remaining_quantity) < netWeight || Number(lot.total_quantity) < netWeight) {
|
| 588 |
+
await client.query('ROLLBACK');
|
| 589 |
+
return res.status(409).json({
|
| 590 |
+
success: false,
|
| 591 |
+
message: `Cannot revert: stock already used for lot ${lot.lot_number}. Remaining ${lot.remaining_quantity}, required ${netWeight}`
|
| 592 |
+
});
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
await client.query(
|
| 596 |
+
`UPDATE lots
|
| 597 |
+
SET total_quantity = total_quantity - $1,
|
| 598 |
+
remaining_quantity = remaining_quantity - $1,
|
| 599 |
+
status = CASE WHEN remaining_quantity - $1 <= 0 THEN 'sold_out' ELSE 'active' END,
|
| 600 |
+
updated_at = CURRENT_TIMESTAMP
|
| 601 |
+
WHERE id = $2`,
|
| 602 |
+
[netWeight, item.lot_id]
|
| 603 |
+
);
|
| 604 |
+
} else if (tx.bill_type === 'awaak' && tx.is_return) {
|
| 605 |
+
// Reverting a purchase return: add stock back
|
| 606 |
+
await client.query(
|
| 607 |
+
`UPDATE lots
|
| 608 |
+
SET total_quantity = total_quantity + $1,
|
| 609 |
+
remaining_quantity = remaining_quantity + $1,
|
| 610 |
+
status = 'active',
|
| 611 |
+
updated_at = CURRENT_TIMESTAMP
|
| 612 |
+
WHERE id = $2`,
|
| 613 |
+
[netWeight, item.lot_id]
|
| 614 |
+
);
|
| 615 |
+
} else if (tx.bill_type === 'jawaak' && !tx.is_return) {
|
| 616 |
+
// Reverting a sale: add stock back to remaining (gross)
|
| 617 |
+
await client.query(
|
| 618 |
+
`UPDATE lots
|
| 619 |
+
SET remaining_quantity = remaining_quantity + $1,
|
| 620 |
+
status = CASE WHEN remaining_quantity + $1 > 0 THEN 'active' ELSE status END,
|
| 621 |
+
updated_at = CURRENT_TIMESTAMP
|
| 622 |
+
WHERE id = $2`,
|
| 623 |
+
[grossWeight, item.lot_id]
|
| 624 |
+
);
|
| 625 |
+
} else if (tx.bill_type === 'jawaak' && tx.is_return) {
|
| 626 |
+
// Reverting a sales return: subtract stock that was added back (gross)
|
| 627 |
+
if (Number(lot.remaining_quantity) < grossWeight) {
|
| 628 |
+
await client.query('ROLLBACK');
|
| 629 |
+
return res.status(409).json({
|
| 630 |
+
success: false,
|
| 631 |
+
message: `Cannot revert: not enough remaining stock in lot ${lot.lot_number}. Remaining ${lot.remaining_quantity}, required ${grossWeight}`
|
| 632 |
+
});
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
await client.query(
|
| 636 |
+
`UPDATE lots
|
| 637 |
+
SET remaining_quantity = remaining_quantity - $1,
|
| 638 |
+
status = CASE WHEN remaining_quantity - $1 <= 0 THEN 'sold_out' ELSE 'active' END,
|
| 639 |
+
updated_at = CURRENT_TIMESTAMP
|
| 640 |
+
WHERE id = $2`,
|
| 641 |
+
[grossWeight, item.lot_id]
|
| 642 |
+
);
|
| 643 |
+
}
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
// Rollback party balance (inverse of creation logic in this file)
|
| 647 |
+
const balanceAmount = Number(tx.balance_amount || 0);
|
| 648 |
+
|
| 649 |
+
if (tx.bill_type === 'jawaak') {
|
| 650 |
+
if (tx.is_return) {
|
| 651 |
+
await client.query(
|
| 652 |
+
'UPDATE parties SET current_balance = current_balance - $1 WHERE id = $2',
|
| 653 |
+
[balanceAmount, tx.party_id]
|
| 654 |
+
);
|
| 655 |
+
} else {
|
| 656 |
+
await client.query(
|
| 657 |
+
'UPDATE parties SET current_balance = current_balance + $1 WHERE id = $2',
|
| 658 |
+
[balanceAmount, tx.party_id]
|
| 659 |
+
);
|
| 660 |
+
}
|
| 661 |
+
} else {
|
| 662 |
+
if (tx.is_return) {
|
| 663 |
+
await client.query(
|
| 664 |
+
'UPDATE parties SET current_balance = current_balance + $1 WHERE id = $2',
|
| 665 |
+
[balanceAmount, tx.party_id]
|
| 666 |
+
);
|
| 667 |
+
} else {
|
| 668 |
+
await client.query(
|
| 669 |
+
'UPDATE parties SET current_balance = current_balance - $1 WHERE id = $2',
|
| 670 |
+
[balanceAmount, tx.party_id]
|
| 671 |
+
);
|
| 672 |
+
}
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
// Delete transaction (items/expenses/payments will cascade delete)
|
| 676 |
+
await client.query('DELETE FROM transactions WHERE id = $1', [id]);
|
| 677 |
+
|
| 678 |
+
await client.query('COMMIT');
|
| 679 |
+
|
| 680 |
+
res.json({
|
| 681 |
+
success: true,
|
| 682 |
+
message: 'Transaction reverted successfully'
|
| 683 |
+
});
|
| 684 |
+
} catch (error) {
|
| 685 |
+
await client.query('ROLLBACK');
|
| 686 |
+
console.error('Error reverting transaction:', error);
|
| 687 |
+
res.status(500).json({ success: false, message: error.message });
|
| 688 |
+
} finally {
|
| 689 |
+
client.release();
|
| 690 |
+
}
|
| 691 |
+
});
|
| 692 |
+
|
| 693 |
module.exports = router;
|