diff --git a/packages/server/src/api/controllers/FinancialStatements/APAgingSummary.ts b/packages/server/src/api/controllers/FinancialStatements/APAgingSummary.ts index 8bd6c1014..cf84377f1 100644 --- a/packages/server/src/api/controllers/FinancialStatements/APAgingSummary.ts +++ b/packages/server/src/api/controllers/FinancialStatements/APAgingSummary.ts @@ -33,10 +33,13 @@ export default class APAgingSummaryReportController extends BaseFinancialReportC return [ ...this.sheetNumberFormatValidationSchema, query('as_date').optional().isISO8601(), - query('aging_days_before').optional().isNumeric().toInt(), - query('aging_periods').optional().isNumeric().toInt(), + + query('aging_days_before').default(30).isNumeric().toInt(), + query('aging_periods').default(3).isNumeric().toInt(), + query('vendors_ids').optional().isArray({ min: 1 }), query('vendors_ids.*').isInt({ min: 1 }).toInt(), + query('none_zero').default(true).isBoolean().toBoolean(), // Filtering by branches. @@ -53,15 +56,36 @@ export default class APAgingSummaryReportController extends BaseFinancialReportC const filter = this.matchedQueryData(req); try { - const { data, columns, query, meta } = - await this.APAgingSummaryService.APAgingSummary(tenantId, filter); + const accept = this.accepts(req); + const acceptType = accept.types(['json', 'application/json+table']); - return res.status(200).send({ - data: this.transfromToResponse(data), - columns: this.transfromToResponse(columns), - query: this.transfromToResponse(query), - meta: this.transfromToResponse(meta), - }); + switch (acceptType) { + case 'application/json+table': + const table = await this.APAgingSummaryService.APAgingSummaryTable( + tenantId, + filter + ); + return res.status(200).send({ + table: { + rows: table.rows, + columns: table.columns, + }, + meta: table.meta, + query: table.query, + }); + break; + default: + const { data, columns, query, meta } = + await this.APAgingSummaryService.APAgingSummary(tenantId, filter); + + return res.status(200).send({ + data: this.transfromToResponse(data), + columns: this.transfromToResponse(columns), + query: this.transfromToResponse(query), + meta: this.transfromToResponse(meta), + }); + break; + } } catch (error) { next(error); } diff --git a/packages/server/src/api/controllers/FinancialStatements/ARAgingSummary.ts b/packages/server/src/api/controllers/FinancialStatements/ARAgingSummary.ts index deb69a172..489eb04ae 100644 --- a/packages/server/src/api/controllers/FinancialStatements/ARAgingSummary.ts +++ b/packages/server/src/api/controllers/FinancialStatements/ARAgingSummary.ts @@ -36,8 +36,8 @@ export default class ARAgingSummaryReportController extends BaseFinancialReportC query('as_date').optional().isISO8601(), - query('aging_days_before').optional().isInt({ max: 500 }).toInt(), - query('aging_periods').optional().isInt({ max: 12 }).toInt(), + query('aging_days_before').default(30).isInt({ max: 500 }).toInt(), + query('aging_periods').default(3).isInt({ max: 12 }).toInt(), query('customers_ids').optional().isArray({ min: 1 }), query('customers_ids.*').isInt({ min: 1 }).toInt(), @@ -58,15 +58,36 @@ export default class ARAgingSummaryReportController extends BaseFinancialReportC const filter = this.matchedQueryData(req); try { - const { data, columns, query, meta } = - await this.ARAgingSummaryService.ARAgingSummary(tenantId, filter); + const accept = this.accepts(req); + const acceptType = accept.types(['json', 'application/json+table']); - return res.status(200).send({ - data: this.transfromToResponse(data), - columns: this.transfromToResponse(columns), - query: this.transfromToResponse(query), - meta: this.transfromToResponse(meta), - }); + switch (acceptType) { + case 'application/json+table': + const table = await this.ARAgingSummaryService.ARAgingSummaryTable( + tenantId, + filter + ); + return res.status(200).send({ + table: { + rows: table.rows, + columns: table.columns, + }, + meta: table.meta, + query: table.query, + }); + break; + default: + const { data, columns, query, meta } = + await this.ARAgingSummaryService.ARAgingSummary(tenantId, filter); + + return res.status(200).send({ + data: this.transfromToResponse(data), + columns: this.transfromToResponse(columns), + query: this.transfromToResponse(query), + meta: this.transfromToResponse(meta), + }); + break; + } } catch (error) { console.log(error); } diff --git a/packages/server/src/interfaces/APAgingSummaryReport.ts b/packages/server/src/interfaces/APAgingSummaryReport.ts index 788892112..db6626167 100644 --- a/packages/server/src/interfaces/APAgingSummaryReport.ts +++ b/packages/server/src/interfaces/APAgingSummaryReport.ts @@ -1,51 +1,36 @@ import { IAgingPeriod, IAgingPeriodTotal, - IAgingAmount + IAgingAmount, + IAgingSummaryQuery, + IAgingSummaryTotal, + IAgingSummaryContact, + IAgingSummaryData, } from './AgingReport'; -import { - INumberFormatQuery -} from './FinancialStatements'; +import { INumberFormatQuery } from './FinancialStatements'; -export interface IAPAgingSummaryQuery { - asDate: Date | string; - agingDaysBefore: number; - agingPeriods: number; - numberFormat: INumberFormatQuery; +export interface IAPAgingSummaryQuery extends IAgingSummaryQuery { vendorsIds: number[]; - noneZero: boolean; - - branchesIds?: number[] } -export interface IAPAgingSummaryVendor { - vendorName: string, - current: IAgingAmount, - aging: IAgingPeriodTotal[], - total: IAgingAmount, -}; +export interface IAPAgingSummaryVendor extends IAgingSummaryContact { + vendorName: string; +} -export interface IAPAgingSummaryTotal { - current: IAgingAmount, - aging: IAgingPeriodTotal[], - total: IAgingAmount, -}; +export interface IAPAgingSummaryTotal extends IAgingSummaryTotal {} -export interface IAPAgingSummaryData { - vendors: IAPAgingSummaryVendor[], - total: IAPAgingSummaryTotal, -}; +export interface IAPAgingSummaryData extends IAgingSummaryData { + vendors: IAPAgingSummaryVendor[]; +} export type IAPAgingSummaryColumns = IAgingPeriod[]; - export interface IARAgingSummaryMeta { - baseCurrency: string, - organizationName: string, + baseCurrency: string; + organizationName: string; } - export interface IAPAgingSummaryMeta { - baseCurrency: string, - organizationName: string, -} \ No newline at end of file + baseCurrency: string; + organizationName: string; +} diff --git a/packages/server/src/interfaces/ARAgingSummaryReport.ts b/packages/server/src/interfaces/ARAgingSummaryReport.ts index a9d6ff3f5..7d25e2b2c 100644 --- a/packages/server/src/interfaces/ARAgingSummaryReport.ts +++ b/packages/server/src/interfaces/ARAgingSummaryReport.ts @@ -1,37 +1,28 @@ -import { IAgingPeriod, IAgingPeriodTotal, IAgingAmount } from './AgingReport'; -import { INumberFormatQuery } from './FinancialStatements'; +import { + IAgingPeriod, + IAgingSummaryQuery, + IAgingSummaryTotal, + IAgingSummaryContact, + IAgingSummaryData, +} from './AgingReport'; -export interface IARAgingSummaryQuery { - asDate: Date | string; - agingDaysBefore: number; - agingPeriods: number; - numberFormat: INumberFormatQuery; +export interface IARAgingSummaryQuery extends IAgingSummaryQuery { customersIds: number[]; - branchesIds: number[]; - noneZero: boolean; } -export interface IARAgingSummaryCustomer { +export interface IARAgingSummaryCustomer extends IAgingSummaryContact { customerName: string; - current: IAgingAmount; - aging: IAgingPeriodTotal[]; - total: IAgingAmount; } -export interface IARAgingSummaryTotal { - current: IAgingAmount; - aging: IAgingPeriodTotal[]; - total: IAgingAmount; -} +export interface IARAgingSummaryTotal extends IAgingSummaryTotal {} -export interface IARAgingSummaryData { +export interface IARAgingSummaryData extends IAgingSummaryData { customers: IARAgingSummaryCustomer[]; - total: IARAgingSummaryTotal; } export type IARAgingSummaryColumns = IAgingPeriod[]; export interface IARAgingSummaryMeta { - organizationName: string, - baseCurrency: string, -} \ No newline at end of file + organizationName: string; + baseCurrency: string; +} diff --git a/packages/server/src/interfaces/AgingReport.ts b/packages/server/src/interfaces/AgingReport.ts index 65983d44f..c68b6b389 100644 --- a/packages/server/src/interfaces/AgingReport.ts +++ b/packages/server/src/interfaces/AgingReport.ts @@ -1,6 +1,9 @@ + +import { INumberFormatQuery } from './FinancialStatements'; + export interface IAgingPeriodTotal extends IAgingPeriod { total: IAgingAmount; -}; +} export interface IAgingAmount { amount: number; @@ -20,3 +23,22 @@ export interface IAgingSummaryContact { aging: IAgingPeriodTotal[]; total: IAgingAmount; } + +export interface IAgingSummaryQuery { + asDate: Date | string; + agingDaysBefore: number; + agingPeriods: number; + numberFormat: INumberFormatQuery; + branchesIds: number[]; + noneZero: boolean; +} + +export interface IAgingSummaryTotal { + current: IAgingAmount; + aging: IAgingPeriodTotal[]; + total: IAgingAmount; +} + +export interface IAgingSummaryData { + total: IAgingSummaryTotal; +} diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryService.ts b/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryService.ts index ad85b1688..a2589745d 100644 --- a/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryService.ts +++ b/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryService.ts @@ -5,6 +5,7 @@ import TenancyService from '@/services/Tenancy/TenancyService'; import APAgingSummarySheet from './APAgingSummarySheet'; import { Tenant } from '@/system/models'; import { isEmpty } from 'lodash'; +import APAgingSummaryTable from './APAgingSummaryTable'; @Service() export default class PayableAgingSummaryService { @@ -84,7 +85,7 @@ export default class PayableAgingSummaryService { // Common query. const commonQuery = (query) => { - if (isEmpty(filter.branchesIds)) { + if (!isEmpty(filter.branchesIds)) { query.modify('filterByBranches', filter.branchesIds); } }; @@ -118,4 +119,22 @@ export default class PayableAgingSummaryService { meta: this.reportMetadata(tenantId), }; } + + /** + * + * @param {number} tenantId + * @param {IAPAgingSummaryQuery} query + * @returns + */ + async APAgingSummaryTable(tenantId: number, query: IAPAgingSummaryQuery) { + const report = await this.APAgingSummary(tenantId, query); + const table = new APAgingSummaryTable(report.data, query, {}); + + return { + columns: table.tableColumns(), + rows: table.tableRows(), + meta: report.meta, + query: report.query, + }; + } } diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryTable.ts b/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryTable.ts new file mode 100644 index 000000000..b74e748d7 --- /dev/null +++ b/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryTable.ts @@ -0,0 +1,46 @@ +import { + IAPAgingSummaryData, + IAgingSummaryQuery, + ITableColumn, + ITableColumnAccessor, + ITableRow, +} from '@/interfaces'; +import AgingSummaryTable from './AgingSummaryTable'; + +export default class APAgingSummaryTable extends AgingSummaryTable { + readonly report: IAPAgingSummaryData; + + /** + * Constructor method. + * @param {IARAgingSummaryData} data + * @param {IAgingSummaryQuery} query + * @param {any} i18n + */ + constructor(data: IAPAgingSummaryData, query: IAgingSummaryQuery, i18n: any) { + super(data, query, i18n); + } + + /** + * Retrieves the contacts table rows. + * @returns {ITableRow[]} + */ + get contactsRows(): ITableRow[] { + return this.contactsNodes(this.report.vendors); + } + + /** + * Contact name node accessor. + * @returns {ITableColumnAccessor} + */ + get contactNameNodeAccessor(): ITableColumnAccessor { + return { key: 'vendor_name', accessor: 'vendorName' }; + } + + /** + * Retrieves the contact name table column. + * @returns {ITableColumn} + */ + contactNameTableColumn = (): ITableColumn => { + return { label: 'Vendor name', key: 'vendor_name' }; + }; +} diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryService.ts b/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryService.ts index b1a5764af..b47bd50a4 100644 --- a/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryService.ts +++ b/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryService.ts @@ -5,6 +5,7 @@ import { IARAgingSummaryQuery, IARAgingSummaryMeta } from '@/interfaces'; import TenancyService from '@/services/Tenancy/TenancyService'; import ARAgingSummarySheet from './ARAgingSummarySheet'; import { Tenant } from '@/system/models'; +import ARAgingSummaryTable from './ARAgingSummaryTable'; @Service() export default class ARAgingSummaryService { @@ -89,12 +90,12 @@ export default class ARAgingSummaryService { }; // Retrieve all overdue sale invoices. const overdueSaleInvoices = await SaleInvoice.query() - .modify('dueInvoicesFromDate', filter.asDate) + .modify('overdueInvoicesFromDate', filter.asDate) .onBuild(commonQuery); // Retrieve all due sale invoices. const currentInvoices = await SaleInvoice.query() - .modify('overdueInvoicesFromDate', filter.asDate) + .modify('dueInvoicesFromDate', filter.asDate) .onBuild(commonQuery); // AR aging summary report instance. @@ -117,4 +118,22 @@ export default class ARAgingSummaryService { meta: this.reportMetadata(tenantId), }; } + + /** + * + * @param tenantId + * @param query + * @returns + */ + async ARAgingSummaryTable(tenantId: number, query: IARAgingSummaryQuery) { + const report = await this.ARAgingSummary(tenantId, query); + const table = new ARAgingSummaryTable(report.data, query, {}); + + return { + columns: table.tableColumns(), + rows: table.tableRows(), + meta: report.meta, + query, + }; + } } diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummarySheet.ts b/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummarySheet.ts index 0dba17a1e..a9f1856c2 100644 --- a/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummarySheet.ts +++ b/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummarySheet.ts @@ -1,4 +1,4 @@ -import { groupBy, isEmpty, sum } from 'lodash'; +import { Dictionary, groupBy, isEmpty, sum } from 'lodash'; import * as R from 'ramda'; import { ICustomer, @@ -54,7 +54,6 @@ export default class ARAgingSummarySheet extends AgingSummaryReport { currentSaleInvoices, 'customerId' ); - // Initializes the aging periods. this.agingPeriods = this.agingRangePeriods( this.query.asDate, @@ -189,7 +188,7 @@ export default class ARAgingSummarySheet extends AgingSummaryReport { }; /** - * Retrieve AR aging summary report columns. + * Retrieve A/R aging summary report columns. * @return {IARAgingSummaryColumns} */ public reportColumns(): IARAgingSummaryColumns { diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryTable.ts b/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryTable.ts new file mode 100644 index 000000000..5e0ad88b3 --- /dev/null +++ b/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryTable.ts @@ -0,0 +1,38 @@ +import { + IARAgingSummaryData, + IAgingSummaryData, + IAgingSummaryQuery, + ITableColumnAccessor, + ITableRow, +} from '@/interfaces'; +import AgingSummaryTable from './AgingSummaryTable'; + +export default class ARAgingSummaryTable extends AgingSummaryTable { + readonly report: IARAgingSummaryData; + + /** + * Constructor method. + * @param {IARAgingSummaryData} data + * @param {IAgingSummaryQuery} query + * @param {any} i18n + */ + constructor(data: IARAgingSummaryData, query: IAgingSummaryQuery, i18n: any) { + super(data, query, i18n); + } + + /** + * Retrieves the contacts table rows. + * @returns {ITableRow[]} + */ + get contactsRows(): ITableRow[] { + return this.contactsNodes(this.report.customers); + } + + /** + * Contact name node accessor. + * @returns {ITableColumnAccessor} + */ + get contactNameNodeAccessor(): ITableColumnAccessor { + return { key: 'customer_name', accessor: 'customerName' }; + } +} diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/AgingSummaryTable.ts b/packages/server/src/services/FinancialStatements/AgingSummary/AgingSummaryTable.ts new file mode 100644 index 000000000..a318cdfc4 --- /dev/null +++ b/packages/server/src/services/FinancialStatements/AgingSummary/AgingSummaryTable.ts @@ -0,0 +1,203 @@ +import * as R from 'ramda'; +import { + IAgingPeriod, + IAgingSummaryContact, + IAgingSummaryData, + IAgingSummaryQuery, + IAgingSummaryTotal, + ITableColumn, + ITableColumnAccessor, + ITableRow, +} from '@/interfaces'; +import { tableRowMapper } from '@/utils'; +import AgingReport from './AgingReport'; +import { AgingSummaryRowType } from './_constants'; + +export default abstract class AgingSummaryTable extends AgingReport { + protected readonly report: IAgingSummaryData; + protected readonly query: IAgingSummaryQuery; + protected readonly agingPeriods: IAgingPeriod[]; + protected readonly i18n: any; + + /** + * Constructor method. + * @param {IARAgingSummaryData} data + * @param {IAgingSummaryQuery} query + * @param {any} i18n + */ + constructor(data: IAgingSummaryData, query: IAgingSummaryQuery, i18n: any) { + super(); + + this.report = data; + this.i18n = i18n; + this.query = query; + + this.agingPeriods = this.agingRangePeriods( + this.query.asDate, + this.query.agingDaysBefore, + this.query.agingPeriods + ); + } + + // ------------------------- + // # Accessors. + // ------------------------- + /** + * Aging accessors of contact and total nodes. + * @param {IAgingSummaryContact | IAgingSummaryTotal} node + * @returns {ITableColumnAccessor[]} + */ + protected agingNodeAccessors = ( + node: IAgingSummaryContact | IAgingSummaryTotal + ): ITableColumnAccessor[] => { + return node.aging.map((aging, index) => ({ + key: 'aging', + accessor: `aging[${index}].total.formattedAmount`, + })); + }; + + /** + * Contact name node accessor. + * @returns {ITableColumnAccessor} + */ + protected get contactNameNodeAccessor(): ITableColumnAccessor { + return { key: 'customer_name', accessor: 'customerName' }; + } + + /** + * Retrieves the common columns for all report nodes. + * @param {IAgingSummaryContact} + * @returns {ITableColumnAccessor[]} + */ + protected contactNodeAccessors = ( + node: IAgingSummaryContact + ): ITableColumnAccessor[] => { + return R.compose( + R.concat([ + this.contactNameNodeAccessor, + { key: 'current', accessor: 'current.formattedAmount' }, + ...this.agingNodeAccessors(node), + { key: 'total', accessor: 'total.formattedAmount' }, + ]) + )([]); + }; + + /** + * Retrieves the contact name table row. + * @param {IAgingSummaryContact} node - + * @return {ITableRow} + */ + protected contactNameNode = (node: IAgingSummaryContact): ITableRow => { + const columns = this.contactNodeAccessors(node); + const meta = { + rowTypes: [AgingSummaryRowType.Contact], + }; + return tableRowMapper(node, columns, meta); + }; + + /** + * Maps the customers nodes to table rows. + * @param {IAgingSummaryContact[]} nodes + * @returns {ITableRow[]} + */ + protected contactsNodes = (nodes: IAgingSummaryContact[]): ITableRow[] => { + return nodes.map(this.contactNameNode); + }; + + /** + * Retrieves the common columns for all report nodes. + * @param {IAgingSummaryTotal} + * @returns {ITableColumnAccessor[]} + */ + protected totalNodeAccessors = ( + node: IAgingSummaryTotal + ): ITableColumnAccessor[] => { + return R.compose( + R.concat([ + { key: 'blank', value: '' }, + { key: 'current', accessor: 'current.formattedAmount' }, + ...this.agingNodeAccessors(node), + { key: 'total', accessor: 'total.formattedAmount' }, + ]) + )([]); + }; + + /** + * Retrieves the total row of the given report total node. + * @param {IAgingSummaryTotal} node + * @returns {ITableRow} + */ + protected totalNode = (node: IAgingSummaryTotal): ITableRow => { + const columns = this.totalNodeAccessors(node); + const meta = { + rowTypes: [AgingSummaryRowType.Total], + }; + return tableRowMapper(node, columns, meta); + }; + + // ------------------------- + // # Computed Rows. + // ------------------------- + /** + * Retrieves the contacts table rows. + * @returns {ITableRow[]} + */ + protected get contactsRows(): ITableRow[] { + return []; + } + + /** + * Table total row. + * @returns {ITableRow} + */ + protected get totalRow(): ITableRow { + return this.totalNode(this.report.total); + } + + /** + * Retrieves the table rows. + * @returns {ITableRow[]} + */ + public tableRows = (): ITableRow[] => { + return R.compose(R.concat(this.contactsRows), R.prepend(this.totalRow))([]); + }; + + // ------------------------- + // # Columns. + // ------------------------- + /** + * Retrieves the aging table columns. + * @returns {ITableColumn[]} + */ + protected agingTableColumns = (): ITableColumn[] => { + return this.agingPeriods.map((agingPeriod) => { + return { + label: `${agingPeriod.beforeDays} - ${ + agingPeriod.toDays || 'And Over' + }`, + key: 'aging_period', + }; + }); + }; + + /** + * Retrieves the contact name table column. + * @returns {ITableColumn} + */ + protected contactNameTableColumn = (): ITableColumn => { + return { label: 'Customer name', key: 'customer_name' }; + }; + + /** + * Retrieves the report columns. + * @returns {ITableColumn} + */ + public tableColumns = (): ITableColumn[] => { + return [ + this.contactNameTableColumn(), + { label: 'Current', key: 'current' }, + ...this.agingTableColumns(), + { label: 'Total', key: 'total' }, + ]; + }; +} diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/_constants.ts b/packages/server/src/services/FinancialStatements/AgingSummary/_constants.ts new file mode 100644 index 000000000..961f0b7ed --- /dev/null +++ b/packages/server/src/services/FinancialStatements/AgingSummary/_constants.ts @@ -0,0 +1,4 @@ +export enum AgingSummaryRowType { + Contact = 'contact', + Total = 'total', +} diff --git a/packages/server/src/services/Purchases/VendorCredits/ApplyVendorCreditToBills/ApplyVendorCreditSyncBills.ts b/packages/server/src/services/Purchases/VendorCredits/ApplyVendorCreditToBills/ApplyVendorCreditSyncBills.ts index f9a86c663..c6f617c60 100644 --- a/packages/server/src/services/Purchases/VendorCredits/ApplyVendorCreditToBills/ApplyVendorCreditSyncBills.ts +++ b/packages/server/src/services/Purchases/VendorCredits/ApplyVendorCreditToBills/ApplyVendorCreditSyncBills.ts @@ -1,13 +1,13 @@ import { Service, Inject } from 'typedi'; -import Knex from 'knex'; +import { Knex } from 'knex'; +import Bluebird from 'bluebird'; import { IVendorCreditAppliedBill } from '@/interfaces'; import HasTenancyService from '@/services/Tenancy/TenancyService'; -import Bluebird from 'bluebird'; @Service() export default class ApplyVendorCreditSyncBills { @Inject() - tenancy: HasTenancyService; + private tenancy: HasTenancyService; /** * Increment bills credited amount. @@ -49,4 +49,4 @@ export default class ApplyVendorCreditSyncBills { .findById(vendorCreditAppliedBill.billId) .decrement('creditedAmount', vendorCreditAppliedBill.amount); }; -} \ No newline at end of file +}