feat: add shipping tax code (#424)
* feat: ✨ add shippingTaxCode * build: 👷 add changeset
This commit is contained in:
parent
9eacc88b53
commit
3347a305cd
11 changed files with 76 additions and 21 deletions
5
.changeset/perfect-lies-float.md
Normal file
5
.changeset/perfect-lies-float.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"saleor-app-taxes": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add "Shipping tax code" field to Avatax configuration form. Allows to customize the tax code set on the "shipping" item.
|
|
@ -17,6 +17,7 @@ const mockedProviders: ProvidersConfig = [
|
||||||
name: "avatax-1",
|
name: "avatax-1",
|
||||||
password: "avatax-password",
|
password: "avatax-password",
|
||||||
username: "avatax-username",
|
username: "avatax-username",
|
||||||
|
shippingTaxCode: "FR000000",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -8,6 +8,7 @@ export const avataxConfigSchema = z.object({
|
||||||
isSandbox: z.boolean(),
|
isSandbox: z.boolean(),
|
||||||
companyCode: z.string().min(1, { message: "Company code requires at least one character." }),
|
companyCode: z.string().min(1, { message: "Company code requires at least one character." }),
|
||||||
isAutocommit: z.boolean(),
|
isAutocommit: z.boolean(),
|
||||||
|
shippingTaxCode: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AvataxConfig = z.infer<typeof avataxConfigSchema>;
|
export type AvataxConfig = z.infer<typeof avataxConfigSchema>;
|
||||||
|
@ -19,6 +20,7 @@ export const defaultAvataxConfig: AvataxConfig = {
|
||||||
companyCode: "",
|
companyCode: "",
|
||||||
isSandbox: true,
|
isSandbox: true,
|
||||||
isAutocommit: false,
|
isAutocommit: false,
|
||||||
|
shippingTaxCode: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const avataxInstanceConfigSchema = z.object({
|
export const avataxInstanceConfigSchema = z.object({
|
||||||
|
|
|
@ -98,6 +98,7 @@ const MOCKED_CALCULATE_TAXES_ARGS: AvataxCalculateTaxesMapPayloadArgs = {
|
||||||
name: "Avatax-1",
|
name: "Avatax-1",
|
||||||
password: "password",
|
password: "password",
|
||||||
username: "username",
|
username: "username",
|
||||||
|
shippingTaxCode: "FR000000",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -114,12 +115,16 @@ describe("avataxCalculateTaxesMaps", () => {
|
||||||
});
|
});
|
||||||
describe("mapLines", () => {
|
describe("mapLines", () => {
|
||||||
it("includes shipping as a line", () => {
|
it("includes shipping as a line", () => {
|
||||||
const lines = avataxCalculateTaxesMaps.mapLines(MOCKED_CALCULATE_TAXES_ARGS.taxBase);
|
const lines = avataxCalculateTaxesMaps.mapLines(
|
||||||
|
MOCKED_CALCULATE_TAXES_ARGS.taxBase,
|
||||||
|
MOCKED_CALCULATE_TAXES_ARGS.config
|
||||||
|
);
|
||||||
|
|
||||||
expect(lines).toContainEqual({
|
expect(lines).toContainEqual({
|
||||||
itemCode: avataxCalculateTaxesMaps.shippingItemCode,
|
itemCode: avataxCalculateTaxesMaps.shippingItemCode,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
amount: 48.33,
|
amount: 48.33,
|
||||||
|
taxCode: MOCKED_CALCULATE_TAXES_ARGS.config.shippingTaxCode,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { avataxAddressFactory } from "./address-factory";
|
||||||
*/
|
*/
|
||||||
const SHIPPING_ITEM_CODE = "Shipping";
|
const SHIPPING_ITEM_CODE = "Shipping";
|
||||||
|
|
||||||
function mapLines(taxBase: TaxBaseFragment): LineItemModel[] {
|
function mapLines(taxBase: TaxBaseFragment, config: AvataxConfig): LineItemModel[] {
|
||||||
const productLines = taxBase.lines.map((line) => ({
|
const productLines = taxBase.lines.map((line) => ({
|
||||||
amount: line.unitPrice.amount,
|
amount: line.unitPrice.amount,
|
||||||
taxIncluded: line.chargeTaxes,
|
taxIncluded: line.chargeTaxes,
|
||||||
|
@ -30,11 +30,7 @@ function mapLines(taxBase: TaxBaseFragment): LineItemModel[] {
|
||||||
const shippingLine: LineItemModel = {
|
const shippingLine: LineItemModel = {
|
||||||
amount: taxBase.shippingPrice.amount,
|
amount: taxBase.shippingPrice.amount,
|
||||||
itemCode: SHIPPING_ITEM_CODE,
|
itemCode: SHIPPING_ITEM_CODE,
|
||||||
/**
|
taxCode: config.shippingTaxCode,
|
||||||
* todo: add taxCode
|
|
||||||
* * Different shipping methods can have different tax codes
|
|
||||||
* https://developer.avalara.com/ecommerce-integration-guide/sales-tax-badge/designing/non-standard-items/\
|
|
||||||
*/
|
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -65,7 +61,7 @@ const mapPayload = (props: AvataxCalculateTaxesMapPayloadArgs): CreateTransactio
|
||||||
shipTo: avataxAddressFactory.fromSaleorAddress(taxBase.address!),
|
shipTo: avataxAddressFactory.fromSaleorAddress(taxBase.address!),
|
||||||
},
|
},
|
||||||
currencyCode: taxBase.currency,
|
currencyCode: taxBase.currency,
|
||||||
lines: mapLines(taxBase),
|
lines: mapLines(taxBase, config),
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
avataxOrderCreatedMaps,
|
avataxOrderCreatedMaps,
|
||||||
} from "./avatax-order-created-map";
|
} from "./avatax-order-created-map";
|
||||||
|
|
||||||
const MOCKED_ORDER: CreateTransactionMapPayloadArgs = {
|
const MOCKED_ARGS: CreateTransactionMapPayloadArgs = {
|
||||||
order: {
|
order: {
|
||||||
id: "T3JkZXI6OTU4MDA5YjQtNDUxZC00NmQ1LThhMWUtMTRkMWRmYjFhNzI5",
|
id: "T3JkZXI6OTU4MDA5YjQtNDUxZC00NmQ1LThhMWUtMTRkMWRmYjFhNzI5",
|
||||||
created: "2023-04-11T11:03:09.304109+00:00",
|
created: "2023-04-11T11:03:09.304109+00:00",
|
||||||
|
@ -120,6 +120,7 @@ const MOCKED_ORDER: CreateTransactionMapPayloadArgs = {
|
||||||
name: "Avatax-1",
|
name: "Avatax-1",
|
||||||
password: "user-password",
|
password: "user-password",
|
||||||
username: "user-name",
|
username: "user-name",
|
||||||
|
shippingTaxCode: "FR000000",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -131,7 +132,7 @@ describe("avataxOrderCreatedMaps", () => {
|
||||||
});
|
});
|
||||||
describe("mapPayload", () => {
|
describe("mapPayload", () => {
|
||||||
it("returns lines with discounted: true when there are discounts", () => {
|
it("returns lines with discounted: true when there are discounts", () => {
|
||||||
const payload = avataxOrderCreatedMaps.mapPayload(MOCKED_ORDER);
|
const payload = avataxOrderCreatedMaps.mapPayload(MOCKED_ARGS);
|
||||||
|
|
||||||
const linesWithoutShipping = payload.model.lines.slice(0, -1);
|
const linesWithoutShipping = payload.model.lines.slice(0, -1);
|
||||||
const check = linesWithoutShipping.every((line) => line.discounted === true);
|
const check = linesWithoutShipping.every((line) => line.discounted === true);
|
||||||
|
@ -143,7 +144,7 @@ describe("avataxOrderCreatedMaps", () => {
|
||||||
it.todo("rounding of numbers");
|
it.todo("rounding of numbers");
|
||||||
});
|
});
|
||||||
describe("mapLines", () => {
|
describe("mapLines", () => {
|
||||||
const lines = avataxOrderCreatedMaps.mapLines(MOCKED_ORDER.order);
|
const lines = avataxOrderCreatedMaps.mapLines(MOCKED_ARGS.order, MOCKED_ARGS.config);
|
||||||
|
|
||||||
it("returns the correct number of lines", () => {
|
it("returns the correct number of lines", () => {
|
||||||
expect(lines).toHaveLength(3);
|
expect(lines).toHaveLength(3);
|
||||||
|
@ -152,6 +153,7 @@ describe("avataxOrderCreatedMaps", () => {
|
||||||
it("includes shipping as a line", () => {
|
it("includes shipping as a line", () => {
|
||||||
expect(lines).toContainEqual({
|
expect(lines).toContainEqual({
|
||||||
itemCode: avataxOrderCreatedMaps.consts.shippingItemCode,
|
itemCode: avataxOrderCreatedMaps.consts.shippingItemCode,
|
||||||
|
taxCode: MOCKED_ARGS.config.shippingTaxCode,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
amount: 48.33,
|
amount: 48.33,
|
||||||
});
|
});
|
||||||
|
@ -176,7 +178,7 @@ describe("avataxOrderCreatedMaps", () => {
|
||||||
});
|
});
|
||||||
describe("mapDiscounts", () => {
|
describe("mapDiscounts", () => {
|
||||||
it("sums up all discounts", () => {
|
it("sums up all discounts", () => {
|
||||||
const discounts = avataxOrderCreatedMaps.mapDiscounts(MOCKED_ORDER.order.discounts);
|
const discounts = avataxOrderCreatedMaps.mapDiscounts(MOCKED_ARGS.order.discounts);
|
||||||
|
|
||||||
expect(discounts).toEqual(31.45);
|
expect(discounts).toEqual(31.45);
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { avataxAddressFactory } from "./address-factory";
|
||||||
*/
|
*/
|
||||||
const SHIPPING_ITEM_CODE = "Shipping";
|
const SHIPPING_ITEM_CODE = "Shipping";
|
||||||
|
|
||||||
function mapLines(order: OrderCreatedSubscriptionFragment): LineItemModel[] {
|
function mapLines(order: OrderCreatedSubscriptionFragment, config: AvataxConfig): LineItemModel[] {
|
||||||
const productLines: LineItemModel[] = order.lines.map((line) => ({
|
const productLines: LineItemModel[] = order.lines.map((line) => ({
|
||||||
amount: line.unitPrice.net.amount,
|
amount: line.unitPrice.net.amount,
|
||||||
// todo: get from tax code matcher
|
// todo: get from tax code matcher
|
||||||
|
@ -32,10 +32,10 @@ function mapLines(order: OrderCreatedSubscriptionFragment): LineItemModel[] {
|
||||||
taxIncluded: true,
|
taxIncluded: true,
|
||||||
itemCode: SHIPPING_ITEM_CODE,
|
itemCode: SHIPPING_ITEM_CODE,
|
||||||
/**
|
/**
|
||||||
* todo: add taxCode
|
|
||||||
* * Different shipping methods can have different tax codes.
|
* * Different shipping methods can have different tax codes.
|
||||||
* https://developer.avalara.com/ecommerce-integration-guide/sales-tax-badge/designing/non-standard-items/\
|
* https://developer.avalara.com/ecommerce-integration-guide/sales-tax-badge/designing/non-standard-items/\
|
||||||
*/
|
*/
|
||||||
|
taxCode: config.shippingTaxCode,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ const mapPayload = ({
|
||||||
},
|
},
|
||||||
currencyCode: order.total.currency,
|
currencyCode: order.total.currency,
|
||||||
email: order.user?.email ?? "",
|
email: order.user?.email ?? "",
|
||||||
lines: mapLines(order),
|
lines: mapLines(order, config),
|
||||||
date: new Date(order.created),
|
date: new Date(order.created),
|
||||||
discount: mapDiscounts(order.discounts),
|
discount: mapDiscounts(order.discounts),
|
||||||
},
|
},
|
||||||
|
|
|
@ -95,6 +95,7 @@ const MOCKED_MAP_PAYLOAD_ARGS: CommitTransactionMapPayloadArgs = {
|
||||||
name: "Avatax-1",
|
name: "Avatax-1",
|
||||||
password: "user-password",
|
password: "user-password",
|
||||||
username: "user-name",
|
username: "user-name",
|
||||||
|
shippingTaxCode: "FR000000",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ const defaultValues: FormValues = {
|
||||||
password: "",
|
password: "",
|
||||||
username: "",
|
username: "",
|
||||||
name: "",
|
name: "",
|
||||||
|
shippingTaxCode: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AvataxConfigurationForm = () => {
|
export const AvataxConfigurationForm = () => {
|
||||||
|
@ -159,7 +160,13 @@ export const AvataxConfigurationForm = () => {
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={defaultValues.name}
|
defaultValue={defaultValues.name}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TextField type="text" {...field} label="Instance name" {...textFieldProps} />
|
<TextField
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
{...field}
|
||||||
|
label="Instance name"
|
||||||
|
{...textFieldProps}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{formState.errors.name && (
|
{formState.errors.name && (
|
||||||
|
@ -227,7 +234,7 @@ export const AvataxConfigurationForm = () => {
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TextField type="text" {...field} label="Username" {...textFieldProps} />
|
<TextField required type="text" {...field} label="Username" {...textFieldProps} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{formState.errors.username && (
|
{formState.errors.username && (
|
||||||
|
@ -239,7 +246,9 @@ export const AvataxConfigurationForm = () => {
|
||||||
name="password"
|
name="password"
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={defaultValues.password}
|
defaultValue={defaultValues.password}
|
||||||
render={({ field }) => <TextField label="Password" {...field} {...textFieldProps} />}
|
render={({ field }) => (
|
||||||
|
<TextField required label="Password" {...field} {...textFieldProps} />
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{formState.errors.password && (
|
{formState.errors.password && (
|
||||||
<FormHelperText error>{formState.errors.password.message}</FormHelperText>
|
<FormHelperText error>{formState.errors.password.message}</FormHelperText>
|
||||||
|
@ -251,13 +260,38 @@ export const AvataxConfigurationForm = () => {
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={defaultValues.companyCode}
|
defaultValue={defaultValues.companyCode}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TextField type="text" {...field} label="Company code" {...textFieldProps} />
|
<TextField
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
{...field}
|
||||||
|
label="Company code"
|
||||||
|
{...textFieldProps}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{formState.errors.companyCode && (
|
{formState.errors.companyCode && (
|
||||||
<FormHelperText error>{formState.errors.companyCode.message}</FormHelperText>
|
<FormHelperText error>{formState.errors.companyCode.message}</FormHelperText>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Controller
|
||||||
|
name="shippingTaxCode"
|
||||||
|
control={control}
|
||||||
|
defaultValue={defaultValues.shippingTaxCode}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField type="text" {...field} label="Shipping tax code" {...textFieldProps} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
{"Tax code that for the shipping line sent to Avatax. "}
|
||||||
|
<AppLink href="https://taxcode.avatax.avalara.com">
|
||||||
|
Must match Avatax tax codes format.
|
||||||
|
</AppLink>
|
||||||
|
</FormHelperText>
|
||||||
|
{formState.errors.shippingTaxCode && (
|
||||||
|
<FormHelperText error>{formState.errors.shippingTaxCode.message}</FormHelperText>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<br />
|
<br />
|
||||||
<div className={styles.reverseRow}>
|
<div className={styles.reverseRow}>
|
||||||
|
|
|
@ -26,6 +26,7 @@ const mockedProviders: ProvidersConfig = [
|
||||||
name: "avatax-1",
|
name: "avatax-1",
|
||||||
password: "avatax-password",
|
password: "avatax-password",
|
||||||
username: "avatax-username",
|
username: "avatax-username",
|
||||||
|
shippingTaxCode: "FR000000",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -162,7 +162,13 @@ export const TaxJarConfigurationForm = () => {
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={defaultValues.name}
|
defaultValue={defaultValues.name}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TextField type="text" {...field} label="Instance name" {...textFieldProps} />
|
<TextField
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
{...field}
|
||||||
|
label="Instance name"
|
||||||
|
{...textFieldProps}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{formState.errors.name && (
|
{formState.errors.name && (
|
||||||
|
@ -174,7 +180,9 @@ export const TaxJarConfigurationForm = () => {
|
||||||
name="apiKey"
|
name="apiKey"
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={defaultValues.apiKey}
|
defaultValue={defaultValues.apiKey}
|
||||||
render={({ field }) => <TextField label="API Key" {...field} {...textFieldProps} />}
|
render={({ field }) => (
|
||||||
|
<TextField required label="API Key" {...field} {...textFieldProps} />
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{formState.errors?.apiKey && (
|
{formState.errors?.apiKey && (
|
||||||
<FormHelperText error>{formState.errors?.apiKey.message}</FormHelperText>
|
<FormHelperText error>{formState.errors?.apiKey.message}</FormHelperText>
|
||||||
|
|
Loading…
Reference in a new issue