Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/35439 integrate vat tax include in price config into service application #13

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ It follows the folder structure to ensure certification & deployment from commer
* insert commercetools credentials to `.env` file
* run `./bin/ngrok.sh` or `ngrok http 8080` to start ngrok and insert the dynamic url in the `.env` file as specified in post-deploy script
* run `yarn connector:post-deploy` to register the extension with the public ngrok url
* run `ỳarn start:dev` to build the application
* run `yarn start:dev` to build the application

## Architecture principles for building an connect application

* Connector solution should be lightweight in nature
* Connector solutions should follow test driven development. Unit , Integration (& E2E) tests should be included and successfully passed to be used
* No hardcoding of customer related config. If needed, values in an environment file which should not be maintained in repository
* Connector solution should be supported with detailed documentation
* Connectors should be point to point in nature, currently doesnt support any persistence capabilities apart from in memory persistence
* Connectors should be point to point in nature, currently doesn't support any persistence capabilities apart from in memory persistence
* Connector solution should use open source technologies, although connector itself can be private for specific customer(s)
* Code should not contain console.log statements, use [the included logger](https://github.com/commercetools/merchant-center-application-kit/tree/main/packages-backend/loggers#readme) instead.
12 changes: 7 additions & 5 deletions event/src/avalara/requests/actions/commit.transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,22 @@ import { AddressInfo } from 'avatax/lib/models/AddressInfo';

export async function commitTransaction(
order: Order,
creds: { [key: string]: string },
credentials: { [key: string]: string },
originAddress: AddressInfo,
config: any
config: any,
pricesIncludesTax: boolean
) {
if (!['US', 'CA'].includes(order?.shippingAddress?.country || 'none')) {
return undefined;
}
const client = new AvaTaxClient(config).withSecurity(creds);
const client = new AvaTaxClient(config).withSecurity(credentials);

const taxDocument = await processOrder(
'commit',
order,
creds?.companyCode,
originAddress
credentials?.companyCode,
originAddress,
pricesIncludesTax
);

const taxResponse = await client.createTransaction({ model: taxDocument });
Expand Down
15 changes: 9 additions & 6 deletions event/src/avalara/requests/actions/refund.transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,35 @@ import { TaxOverrideModel } from 'avatax/lib/models/TaxOverrideModel';
import { getOrder } from '../../../client/data.client';
import { AddressInfo } from 'avatax/lib/models/AddressInfo';
import { processOrder } from '../preprocess/preprocess.order';
import { TaxOverrideType } from 'avatax/lib/enums/TaxOverrideType';

export async function refundTransaction(
orderId: string,
creds: { [key: string]: string },
credentials: { [key: string]: string },
originAddress: AddressInfo,
config: any
config: any,
pricesIncludesTax: boolean
) {
const order = await getOrder(orderId);

if (!['US', 'CA'].includes(order?.shippingAddress?.country || 'none')) {
return undefined;
}
const client = new AvaTaxClient(config).withSecurity(creds);
const client = new AvaTaxClient(config).withSecurity(credentials);

const taxDocument = await processOrder(
'refund',
order,
creds?.companyCode,
originAddress
credentials?.companyCode,
originAddress,
pricesIncludesTax
);

taxDocument.referenceCode = 'Refund';

const taxModel = new TaxOverrideModel();
taxModel.taxDate = new Date(order.createdAt);
taxModel.type = 3;
taxModel.type = TaxOverrideType.TaxDate;
taxModel.reason = 'Refund';
taxDocument.taxOverride = taxModel;

Expand Down
12 changes: 7 additions & 5 deletions event/src/avalara/requests/actions/void.transaction.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
import AvaTaxClient from 'avatax/lib/AvaTaxClient';
import { VoidTransactionModel } from 'avatax/lib/models/VoidTransactionModel';
import { getOrder } from '../../../client/data.client';
import { DocumentType } from 'avatax/lib/enums/DocumentType';
import { VoidReasonCode } from 'avatax/lib/enums/VoidReasonCode';

export async function voidTransaction(
orderId: string,
creds: { [key: string]: string },
credentials: { [key: string]: string },
config: any
) {
const order = await getOrder(orderId);

if (!['US', 'CA'].includes(order?.shippingAddress?.country || 'none')) {
return undefined;
}
const client = new AvaTaxClient(config).withSecurity(creds);
const client = new AvaTaxClient(config).withSecurity(credentials);

const voidModel = new VoidTransactionModel();
voidModel.code = 3;
voidModel.code = VoidReasonCode.DocVoided;
const voidBody = {
companyCode: creds.companyCode,
companyCode: credentials.companyCode,
transactionCode: order?.orderNumber || orderId,
documentType: 1,
documentType: DocumentType.SalesInvoice,
model: voidModel,
};

Expand Down
38 changes: 34 additions & 4 deletions event/src/avalara/requests/preprocess/preprocess.order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,25 @@ import { shippingAddress } from '../../utils/shipping.address';
import { shipItem } from '../../utils/shipping.info';
import { AddressInfo } from 'avatax/lib/models/AddressInfo';
import { getCategoryTaxCodes } from './get.categories';
import { DocumentType } from 'avatax/lib/enums/DocumentType';
import { TransactionParameterModel } from 'avatax/lib/models/TransactionParameterModel';

function extractVatId(order: Order) {
const paymentCustomerVatIds = order?.paymentInfo?.payments.map(
(payment) => payment.obj?.customer?.obj?.vatId
630N marked this conversation as resolved.
Show resolved Hide resolved
);
const vatId = paymentCustomerVatIds?.find((vatId) => vatId !== undefined);

return vatId || order?.customerId;
630N marked this conversation as resolved.
Show resolved Hide resolved
}

// initialize and specify the tax document model of Avalara
export async function processOrder(
type: string,
order: Order,
companyCode: string,
originAddress: AddressInfo
originAddress: AddressInfo,
pricesIncludesTax: boolean
): Promise<CreateTransactionModel> {
const taxDocument = new CreateTransactionModel();

Expand All @@ -21,13 +33,18 @@ export async function processOrder(

const shipTo = shippingAddress(order?.shippingAddress);

const shippingInfo = await shipItem(type, order?.shippingInfo);
const shippingInfo = await shipItem(
type,
order?.shippingInfo,
pricesIncludesTax
);

const itemCategoryTaxCodes = await getCategoryTaxCodes(order?.lineItems);

const lines = await Promise.all(
order?.lineItems.map(
async (x: LineItem) => await lineItem(type, x, itemCategoryTaxCodes)
async (x: LineItem) =>
await lineItem(type, x, itemCategoryTaxCodes, pricesIncludesTax)
)
);

Expand All @@ -45,7 +62,10 @@ export async function processOrder(

taxDocument.companyCode = companyCode;

taxDocument.type = type === 'refund' ? 5 : 1;
taxDocument.type =
type === 'refund'
? DocumentType.ReturnInvoice
: DocumentType.SalesInvoice;

taxDocument.currencyCode = order?.totalPrice?.currencyCode;

Expand All @@ -57,6 +77,16 @@ export async function processOrder(
};
taxDocument.entityUseCode = customerInfo?.exemptCode;
taxDocument.lines = lines;

taxDocument.businessIdentificationNo = extractVatId(order);
taxDocument.parameters = [
{
name: 'Transport',
value:
taxDocument.type === DocumentType.SalesInvoice ? 'Seller' : 'Buyer',
unit: '',
} as TransactionParameterModel,
];
}

return taxDocument;
Expand Down
5 changes: 3 additions & 2 deletions event/src/avalara/utils/line.items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ function itemTaxCode(item: LineItem) {
export async function lineItem(
type: string,
item: LineItem,
catTaxCodes: [{ [key: string]: string }]
catTaxCodes: [{ [key: string]: string }],
pricesIncludesTax: boolean
) {
const lineItem = new LineItemModel();

Expand All @@ -33,7 +34,7 @@ export async function lineItem(

lineItem.itemCode = item?.variant?.sku;

lineItem.taxIncluded = item?.taxRate?.includedInPrice;
lineItem.taxIncluded = pricesIncludesTax;
lineItem.taxCode =
itemTaxCode(item) ??
catTaxCodes?.find((x) => x?.sku === item?.variant?.sku)?.taxCode;
Expand Down
8 changes: 6 additions & 2 deletions event/src/avalara/utils/shipping.info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ import { LineItemModel } from 'avatax/lib/models/LineItemModel';
import { getShipTaxCode } from '../../client/data.client';

// Mapping CT LineItem Model to Avalara LineItem Model
export async function shipItem(type: string, item: ShippingInfo) {
export async function shipItem(
type: string,
item: ShippingInfo,
pricesIncludesTax: boolean
) {
const lineItem = new LineItemModel();
const taxCode = await getShipTaxCode(item.shippingMethod?.id || '');
lineItem.quantity = 1;
lineItem.amount =
((type === 'refund' ? -1 : 1) * item.price.centAmount) / 100;
lineItem.description = item.shippingMethodName;
lineItem.itemCode = 'Shipping';
lineItem.taxIncluded = item.taxRate?.includedInPrice;
lineItem.taxIncluded = pricesIncludesTax;
lineItem.taxCode = taxCode || 'FR010000';

return lineItem;
Expand Down
63 changes: 55 additions & 8 deletions event/src/controllers/event.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { setUpAvaTax } from '../utils/avatax.utils';
import { commitTransaction } from '../avalara/requests/actions/commit.transaction';
import { voidTransaction } from '../avalara/requests/actions/void.transaction';
import { refundTransaction } from '../avalara/requests/actions/refund.transaction';
import { TransactionModel } from 'avatax/lib/models/TransactionModel';
import { Order } from '@commercetools/platform-sdk';
import { TransactionLineModel } from 'avatax/lib/models/TransactionLineModel';

/**
* Exposed event POST endpoint.
Expand Down Expand Up @@ -42,14 +45,55 @@ function parseRequest(request: Request) {
throw new CustomError(400, 'Bad request: No payload in the Pub/Sub message');
}

function buildResponseModel(
orderMessage: OrderCreatedMessage,
transaction: any
) {
if (!(transaction || transaction instanceof TransactionModel)) {
630N marked this conversation as resolved.
Show resolved Hide resolved
return undefined;
}
const lineItems = orderMessage.order?.lineItems?.map((lineItem) => {
return {
...lineItem,
realnaliboh marked this conversation as resolved.
Show resolved Hide resolved
custom: {
type: {
id: 'vatCodeType',
key: 'vatCode',
typeId: 'type',
},
fields: {
vatCode: transaction.lines?.find(
(x: TransactionLineModel) => x.itemCode === lineItem.variant.sku
)?.vatCode,
},
},
};
});
const order = {
630N marked this conversation as resolved.
Show resolved Hide resolved
...orderMessage.order,
lineItems: lineItems,
630N marked this conversation as resolved.
Show resolved Hide resolved
custom: {
type: {
id: 'invoiceMessagesType',
key: 'invoiceMessages',
typeId: 'type',
},
fields: {
invoiceMessages: transaction.invoiceMessages,
},
},
};
return order as Order;
}

export const post = async (
request: Request,
response: Response,
next: NextFunction
) => {
try {
const env = process.env.AVALARA_ENV || 'sandbox';
const creds = {
const credentials = {
username: process.env.AVALARA_USERNAME as string,
password: process.env.AVALARA_PASSWORD as string,
companyCode: process.env.AVALARA_COMPANY_CODE as string,
Expand All @@ -75,13 +119,15 @@ export const post = async (
if (!messagePayload.order) {
throw new CustomError(400, `Order must be defined.`);
}
await commitTransaction(
const transaction = await commitTransaction(
messagePayload.order,
creds,
credentials,
originAddress,
avataxConfig
avataxConfig,
settings.displayPricesWithTax
).catch((error) => logger.error(error));
response.status(200).send();
const updatedOrder = buildResponseModel(messagePayload, transaction);
response.status(200).send(updatedOrder);
630N marked this conversation as resolved.
Show resolved Hide resolved
break;
case 'OrderStateChanged':
if (
Expand All @@ -90,16 +136,17 @@ export const post = async (
) {
await voidTransaction(
messagePayload.resource.id,
creds,
credentials,
avataxConfig
).catch(async (error) => {
logger.error(error);
if (error?.code === 'CannotModifyLockedTransaction') {
await refundTransaction(
messagePayload.resource.id,
creds,
credentials,
originAddress,
avataxConfig
avataxConfig,
settings.displayPricesWithTax
).catch((error) => logger.error(error));
630N marked this conversation as resolved.
Show resolved Hide resolved
}
});
Expand Down
Loading