diff --git a/server/src/api/controllers/Contacts/Customers.ts b/server/src/api/controllers/Contacts/Customers.ts index 14798b95d..382bf5982 100644 --- a/server/src/api/controllers/Contacts/Customers.ts +++ b/server/src/api/controllers/Contacts/Customers.ts @@ -32,6 +32,17 @@ export default class CustomersController extends ContactsController { asyncMiddleware(this.newCustomer.bind(this)), this.handlerServiceErrors ); + router.post( + '/:id/opening_balance', + [ + ...this.specificContactSchema, + check('opening_balance').exists().isNumeric().toFloat(), + check('opening_balance_at').optional().isISO8601(), + ], + this.validationResult, + asyncMiddleware(this.editOpeningBalanceCustomer.bind(this)), + this.handlerServiceErrors, + ); router.post('/:id', [ ...this.contactDTOSchema, ...this.contactEditDTOSchema, @@ -160,6 +171,36 @@ export default class CustomersController extends ContactsController { } } + /** + * Changes the opening balance of the given customer. + * @param {Request} req - + * @param {Response} res - + * @param {NextFunction} next - + */ + async editOpeningBalanceCustomer(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { id: customerId } = req.params; + const { + openingBalance, + openingBalanceAt, + } = this.matchedBodyData(req); + + try { + await this.customersService.changeOpeningBalance( + tenantId, + customerId, + openingBalance, + openingBalanceAt, + ); + return res.status(200).send({ + id: customerId, + message: 'The opening balance of the given customer has been changed successfully.', + }); + } catch (error) { + next(error); + } + } + /** * Deletes the given customer from the storage. * @param {Request} req @@ -283,6 +324,11 @@ export default class CustomersController extends ContactsController { errors: [{ type: 'CUSTOMER.HAS.SALES_INVOICES', code: 400 }], }); } + if (error.errorType === 'OPENING_BALANCE_DATE_REQUIRED') { + return res.boom.badRequest(null, { + errors: [{ type: 'OPENING_BALANCE_DATE_REQUIRED', code: 500 }], + }); + } } next(error); } diff --git a/server/src/api/controllers/Contacts/Vendors.ts b/server/src/api/controllers/Contacts/Vendors.ts index 162ac1c22..d8c80dee2 100644 --- a/server/src/api/controllers/Contacts/Vendors.ts +++ b/server/src/api/controllers/Contacts/Vendors.ts @@ -28,6 +28,17 @@ export default class VendorsController extends ContactsController { asyncMiddleware(this.newVendor.bind(this)), this.handlerServiceErrors, ); + router.post( + '/:id/opening_balance', + [ + ...this.specificContactSchema, + check('opening_balance').exists().isNumeric().toFloat(), + check('opening_balance_at').optional().isISO8601(), + ], + this.validationResult, + asyncMiddleware(this.editOpeningBalanceVendor.bind(this)), + this.handlerServiceErrors, + ); router.post('/:id', [ ...this.contactDTOSchema, ...this.contactEditDTOSchema, @@ -144,6 +155,36 @@ export default class VendorsController extends ContactsController { } } + /** + * Changes the opening balance of the given vendor. + * @param {Request} req - + * @param {Response} res - + * @param {NextFunction} next - + */ + async editOpeningBalanceVendor(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { id: vendorId } = req.params; + const { + openingBalance, + openingBalanceAt, + } = this.matchedBodyData(req); + + try { + await this.vendorsService.changeOpeningBalance( + tenantId, + vendorId, + openingBalance, + openingBalanceAt, + ); + return res.status(200).send({ + id: vendorId, + message: 'The opening balance of the given vendor has been changed successfully.', + }); + } catch (error) { + next(error); + } + } + /** * Deletes the given vendor from the storage. * @param {Request} req @@ -261,6 +302,11 @@ export default class VendorsController extends ContactsController { errors: [{ type: 'VENDOR.HAS.BILLS', code: 400 }], }); } + if (error.errorType === 'OPENING_BALANCE_DATE_REQUIRED') { + return res.boom.badRequest(null, { + errors: [{ type: 'OPENING_BALANCE_DATE_REQUIRED', code: 500 }], + }); + } } next(error); } diff --git a/server/src/services/Contacts/ContactsService.ts b/server/src/services/Contacts/ContactsService.ts index 97500d39a..c9c7f580b 100644 --- a/server/src/services/Contacts/ContactsService.ts +++ b/server/src/services/Contacts/ContactsService.ts @@ -1,5 +1,6 @@ import { Inject, Service } from 'typedi'; import { difference, upperFirst, omit } from 'lodash'; +import moment from 'moment'; import { ServiceError } from "exceptions"; import TenancyService from 'services/Tenancy/TenancyService'; import { @@ -11,6 +12,10 @@ import JournalPoster from '../Accounting/JournalPoster'; type TContactService = 'customer' | 'vendor'; +const ERRORS = { + OPENING_BALANCE_DATE_REQUIRED: 'OPENING_BALANCE_DATE_REQUIRED', +}; + @Service() export default class ContactsService { @Inject() @@ -185,4 +190,40 @@ export default class ContactsService { journal.deleteEntries(), ]); } + + /** + * Chanages the opening balance of the given contact. + * @param {number} tenantId + * @param {number} contactId + * @param {ICustomerChangeOpeningBalanceDTO} changeOpeningBalance + * @return {Promise} + */ + public async changeOpeningBalance( + tenantId: number, + contactId: number, + contactService: string, + openingBalance: number, + openingBalanceAt?: Date|string, + ): Promise { + const { contactRepository } = this.tenancy.repositories(tenantId); + + // Retrieve the given contact details or throw not found service error. + const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService); + + // Should the opening balance date be required. + if (!contact.openingBalanceAt && !openingBalanceAt) { + throw new ServiceError(ERRORS.OPENING_BALANCE_DATE_REQUIRED); + }; + // Changes the customer the opening balance and opening balance date. + await contactRepository.update({ + openingBalance: openingBalance, + + ...(openingBalanceAt) && ({ + openingBalanceAt: moment(openingBalanceAt).toMySqlDateTime(), + }), + }, { + id: contactId, + contactService, + }); + } } \ No newline at end of file diff --git a/server/src/services/Contacts/CustomersService.ts b/server/src/services/Contacts/CustomersService.ts index 9ee8b30ed..43c2dd867 100644 --- a/server/src/services/Contacts/CustomersService.ts +++ b/server/src/services/Contacts/CustomersService.ts @@ -16,14 +16,13 @@ import { IContactNewDTO, IContactEditDTO, IContact, - ISaleInvoice + ISaleInvoice, } from 'interfaces'; import { ServiceError } from 'exceptions'; import TenancyService from 'services/Tenancy/TenancyService'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import events from 'subscribers/events'; import moment from 'moment'; -import SaleInvoiceRepository from 'repositories/SaleInvoiceRepository'; @Service() export default class CustomersService { @@ -298,4 +297,31 @@ export default class CustomersService { throw new ServiceError('some_customers_have_invoices'); } } + + /** + * Changes the opening balance of the given customer. + * @param {number} tenantId + * @param {number} customerId + * @param {number} openingBalance + * @param {string|Date} openingBalanceAt + */ + public async changeOpeningBalance( + tenantId: number, + customerId: number, + openingBalance: number, + openingBalanceAt: Date|string, + ) { + + await this.contactService.changeOpeningBalance( + tenantId, + customerId, + 'customer', + openingBalance, + openingBalanceAt, + ); + // Triggers `onOpeingBalanceChanged` event. + await this.eventDispatcher.dispatch(events.customers.onOpeningBalanceChanged, { + tenantId, customerId, openingBalance, openingBalanceAt + }); + } } diff --git a/server/src/services/Contacts/VendorsService.ts b/server/src/services/Contacts/VendorsService.ts index 16982d750..9c34ad725 100644 --- a/server/src/services/Contacts/VendorsService.ts +++ b/server/src/services/Contacts/VendorsService.ts @@ -251,4 +251,31 @@ export default class VendorsService { filterMeta: dynamicFilter.getResponseMeta(), }; } + + /** + * Changes the opeing balance of the given vendor. + * @param {number} tenantId + * @param {number} vendorId + * @param {number} openingBalance + * @param {Date|string} openingBalanceAt + */ + public async changeOpeningBalance( + tenantId: number, + vendorId: number, + openingBalance: number, + openingBalanceAt: Date|string, + ): Promise { + + await this.contactService.changeOpeningBalance( + tenantId, + vendorId, + 'vendor', + openingBalance, + openingBalanceAt, + ); + // Triggers `onOpeingBalanceChanged` event. + await this.eventDispatcher.dispatch(events.vendors.onOpeningBalanceChanged, { + tenantId, vendorId, openingBalance, openingBalanceAt + }); + } } diff --git a/server/src/services/Items/ItemsService.ts b/server/src/services/Items/ItemsService.ts index b729e0d83..4e8394579 100644 --- a/server/src/services/Items/ItemsService.ts +++ b/server/src/services/Items/ItemsService.ts @@ -19,7 +19,7 @@ const ERRORS = { ITEMS_HAVE_ASSOCIATED_TRANSACTIONS: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS', ITEM_HAS_ASSOCIATED_TRANSACTINS: 'ITEM_HAS_ASSOCIATED_TRANSACTINS' -} +}; @Service() export default class ItemsService implements IItemsService { diff --git a/server/src/subscribers/events.ts b/server/src/subscribers/events.ts index 0bd55d059..10aa2f529 100644 --- a/server/src/subscribers/events.ts +++ b/server/src/subscribers/events.ts @@ -146,6 +146,7 @@ export default { onEdited: 'onCustomerEdited', onDeleted: 'onCustomerDeleted', onBulkDeleted: 'onBulkDeleted', + onOpeningBalanceChanged: 'onOpeingBalanceChanged', }, /** @@ -156,6 +157,7 @@ export default { onEdited: 'onVendorEdited', onDeleted: 'onVendorDeleted', onBulkDeleted: 'onVendorBulkDeleted', + onOpeningBalanceChanged: 'onOpeingBalanceChanged', }, items: {