Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const mongoose = require('mongoose');
const Model = mongoose.model('Invoice');

const { calculate } = require('@/helpers');
const { increaseBySettingKey } = require('@/middlewares/settings');
const { getNextSequence } = require('@/middlewares/sequence');
const schema = require('./schemaValidate');

const create = async (req, res) => {
Expand Down Expand Up @@ -47,6 +47,13 @@ const create = async (req, res) => {
body['paymentStatus'] = paymentStatus;
body['createdBy'] = req.admin._id;

// Atomically allocate the invoice number/year so concurrent creates can never
// collide; the client-provided values are ignored in favour of the sequence.
const year = value.year || new Date(value.date).getFullYear();
const sequence = await getNextSequence({ entity: 'invoice', year });
body['number'] = sequence.number;
body['year'] = sequence.year;

// Creating a new document in the collection
const result = await new Model(body).save();
const fileId = 'invoice-' + result._id + '.pdf';
Expand All @@ -57,11 +64,6 @@ const create = async (req, res) => {
new: true,
}
).exec();
// Returning successfull response

increaseBySettingKey({
settingKey: 'last_invoice_number',
});

// Returning successfull response
return res.status(200).json({
Expand Down
26 changes: 26 additions & 0 deletions idurar-erp-crm/backend/src/middlewares/sequence/getNextSequence.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const mongoose = require('mongoose');

const Model = mongoose.model('Sequence');

// Atomically allocates and returns the next number for a given entity/year.
// The single-document findOneAndUpdate with $inc + upsert is atomic in MongoDB,
// so concurrent creates can never be handed the same number.
const getNextSequence = async ({ entity, year }) => {
const currentYear = year || new Date().getFullYear();

const result = await Model.findOneAndUpdate(
{ entity, year: currentYear },
{
$inc: { current: 1 },
},
{
new: true, // return the incremented document
upsert: true, // create the counter on first use
setDefaultsOnInsert: true,
}
).exec();

return { number: result.current, year: currentYear };
};

module.exports = getNextSequence;
5 changes: 5 additions & 0 deletions idurar-erp-crm/backend/src/middlewares/sequence/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const getNextSequence = require('./getNextSequence');

module.exports = {
getNextSequence,
};
30 changes: 30 additions & 0 deletions idurar-erp-crm/backend/src/models/coreModels/Sequence.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const mongoose = require('mongoose');

// Atomic counter collection used to allocate sequential numbers (e.g. invoice
// and quote numbers). One document per entity/year pair holds the last number
// that was handed out.
const sequenceSchema = new mongoose.Schema({
removed: {
type: Boolean,
default: false,
},
entity: {
type: String,
required: true,
lowercase: true,
},
year: {
type: Number,
required: true,
},
current: {
type: Number,
default: 0,
},
});

// Guarantees a single counter document per entity/year, so the atomic upsert in
// getNextSequence can never create duplicates under concurrency.
sequenceSchema.index({ entity: 1, year: 1 }, { unique: true });

module.exports = mongoose.model('Sequence', sequenceSchema);