39f2c0c15e
**What** Since the release of the Tax API the line item totals calculations on orders with gift cards have been wrong. To understand the bug consider the below order: Region: - tax_rate: 25% - gift_cards_taxable: true Order: - applied gift card: 1000 - items: - A: unit_price: 1000 - B: unit_price: 500 - Subtotal: 1500 **Previous calculation method** 1. Determine how much of the gift card is used for each item using `item_total / subtotal * gift_card_amount`: - Item A: 1000/1500 * 1000 = 666.67 - Item B: 500/1500 * 1000 = 333.33 2. Calculate line item totals including taxes using `(unit_price - gift_card) * (1 + tax_rate)` - Item A: 1000 - 666.67 = 333.33; vat amount -> 83.33 - Item B: 500 - 333.33 = 166.67; vat amount -> 41.67 3. Add up the line item totals: order subtotal = 500; vat amount = 125; total = 625 This is all correct at the totals level; but at the line item level we should still use the "original prices" i.e. the line item total for item a should be (1000 * 1.25) = 1250 with a tax amount of 250. **New calculation method** 1. Use default totals calculations - Item A: subtotal: 1000, tax_total: 250, total: 1250 - Item B: subtotal: 500, tax_total: 125, total: 625 2. Add up the line item totals: subtotal: 1500, tax_total: 375, total: 1875 3. Reduce total with gift card: subtotal: 1500 - 1000 = 500, tax_total: 375 - 250 = 125, total = 625 Totals can now be forwarded correctly to accounting plugins. Fixes CORE-310.
269 lines
6.1 KiB
TypeScript
269 lines
6.1 KiB
TypeScript
/**
|
|
* @schema cart
|
|
* title: "Cart"
|
|
* description: "Represents a user cart"
|
|
* x-resourceId: cart
|
|
* properties:
|
|
* id:
|
|
* type: string
|
|
* email:
|
|
* type: string
|
|
* billing_address_id:
|
|
* type: string
|
|
* billing_address:
|
|
* $ref: "#/components/schemas/address"
|
|
* shipping_address_id:
|
|
* type: string
|
|
* shipping_address:
|
|
* $ref: "#/components/schemas/address"
|
|
* items:
|
|
* type: array
|
|
* items:
|
|
* $ref: "#/components/schemas/line_item"
|
|
* region_id:
|
|
* type: string
|
|
* region:
|
|
* $ref: "#/components/schemas/region"
|
|
* discounts:
|
|
* type: array
|
|
* items:
|
|
* $ref: "#/components/schemas/region"
|
|
* gift_cards:
|
|
* type: array
|
|
* items:
|
|
* $ref: "#/components/schemas/gift_card"
|
|
* customer_id:
|
|
* type: string
|
|
* customer:
|
|
* $ref: "#/components/schemas/customer"
|
|
* payment_session:
|
|
* $ref: "#/components/schemas/payment_session"
|
|
* payment_sessions:
|
|
* type: array
|
|
* items:
|
|
* $ref: "#/components/schemas/payment_session"
|
|
* payment:
|
|
* $ref: "#/components/schemas/payment"
|
|
* shipping_methods:
|
|
* type: array
|
|
* items:
|
|
* $ref: "#/components/schemas/shipping_method"
|
|
* type:
|
|
* type: string
|
|
* enum:
|
|
* - default
|
|
* - swap
|
|
* - payment_link
|
|
* completed_at:
|
|
* type: string
|
|
* format: date-time
|
|
* created_at:
|
|
* type: string
|
|
* format: date-time
|
|
* updated_at:
|
|
* type: string
|
|
* format: date-time
|
|
* deleted_at:
|
|
* type: string
|
|
* format: date-time
|
|
* metadata:
|
|
* type: object
|
|
* shipping_total:
|
|
* type: integer
|
|
* discount_total:
|
|
* type: integer
|
|
* tax_total:
|
|
* type: integer
|
|
* subtotal:
|
|
* type: integer
|
|
* refundable_amount:
|
|
* type: integer
|
|
* gift_card_total:
|
|
* type: integer
|
|
*/
|
|
|
|
import {
|
|
AfterLoad,
|
|
BeforeInsert,
|
|
Column,
|
|
Entity,
|
|
Index,
|
|
JoinColumn,
|
|
JoinTable,
|
|
ManyToMany,
|
|
ManyToOne,
|
|
OneToMany,
|
|
OneToOne,
|
|
} from "typeorm"
|
|
import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column"
|
|
import { Address } from "./address"
|
|
import { Customer } from "./customer"
|
|
import { Discount } from "./discount"
|
|
import { GiftCard } from "./gift-card"
|
|
import { LineItem } from "./line-item"
|
|
import { Payment } from "./payment"
|
|
import { PaymentSession } from "./payment-session"
|
|
import { Region } from "./region"
|
|
import { ShippingMethod } from "./shipping-method"
|
|
import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
|
|
import { generateEntityId } from "../utils/generate-entity-id"
|
|
import {
|
|
FeatureFlagColumn,
|
|
FeatureFlagDecorators,
|
|
} from "../utils/feature-flag-decorators"
|
|
import { SalesChannel } from "./sales-channel"
|
|
|
|
export enum CartType {
|
|
DEFAULT = "default",
|
|
SWAP = "swap",
|
|
DRAFT_ORDER = "draft_order",
|
|
PAYMENT_LINK = "payment_link",
|
|
CLAIM = "claim",
|
|
}
|
|
|
|
@Entity()
|
|
export class Cart extends SoftDeletableEntity {
|
|
readonly object = "cart"
|
|
|
|
@Column({ nullable: true })
|
|
email: string
|
|
|
|
@Index()
|
|
@Column({ nullable: true })
|
|
billing_address_id: string
|
|
|
|
@ManyToOne(() => Address, {
|
|
cascade: ["insert", "remove", "soft-remove"],
|
|
})
|
|
@JoinColumn({ name: "billing_address_id" })
|
|
billing_address: Address
|
|
|
|
@Index()
|
|
@Column({ nullable: true })
|
|
shipping_address_id: string
|
|
|
|
@ManyToOne(() => Address, {
|
|
cascade: ["insert", "remove", "soft-remove"],
|
|
})
|
|
@JoinColumn({ name: "shipping_address_id" })
|
|
shipping_address: Address | null
|
|
|
|
@OneToMany(() => LineItem, (lineItem) => lineItem.cart, {
|
|
cascade: ["insert", "remove"],
|
|
})
|
|
items: LineItem[]
|
|
|
|
@Index()
|
|
@Column()
|
|
region_id: string
|
|
|
|
@ManyToOne(() => Region)
|
|
@JoinColumn({ name: "region_id" })
|
|
region: Region
|
|
|
|
@ManyToMany(() => Discount)
|
|
@JoinTable({
|
|
name: "cart_discounts",
|
|
joinColumn: {
|
|
name: "cart_id",
|
|
referencedColumnName: "id",
|
|
},
|
|
inverseJoinColumn: {
|
|
name: "discount_id",
|
|
referencedColumnName: "id",
|
|
},
|
|
})
|
|
discounts: Discount[]
|
|
|
|
@ManyToMany(() => GiftCard)
|
|
@JoinTable({
|
|
name: "cart_gift_cards",
|
|
joinColumn: {
|
|
name: "cart_id",
|
|
referencedColumnName: "id",
|
|
},
|
|
inverseJoinColumn: {
|
|
name: "gift_card_id",
|
|
referencedColumnName: "id",
|
|
},
|
|
})
|
|
gift_cards: GiftCard[]
|
|
|
|
@Index()
|
|
@Column({ nullable: true })
|
|
customer_id: string
|
|
|
|
@ManyToOne(() => Customer)
|
|
@JoinColumn({ name: "customer_id" })
|
|
customer: Customer
|
|
|
|
payment_session: PaymentSession | null
|
|
|
|
@OneToMany(() => PaymentSession, (paymentSession) => paymentSession.cart, {
|
|
cascade: true,
|
|
})
|
|
payment_sessions: PaymentSession[]
|
|
|
|
@Index()
|
|
@Column({ nullable: true })
|
|
payment_id: string
|
|
|
|
@OneToOne(() => Payment)
|
|
@JoinColumn({ name: "payment_id" })
|
|
payment: Payment
|
|
|
|
@OneToMany(() => ShippingMethod, (method) => method.cart, {
|
|
cascade: ["soft-remove", "remove"],
|
|
})
|
|
shipping_methods: ShippingMethod[]
|
|
|
|
@DbAwareColumn({ type: "enum", enum: CartType, default: "default" })
|
|
type: CartType
|
|
|
|
@Column({ type: resolveDbType("timestamptz"), nullable: true })
|
|
completed_at: Date
|
|
|
|
@Column({ type: resolveDbType("timestamptz"), nullable: true })
|
|
payment_authorized_at: Date
|
|
|
|
@Column({ nullable: true })
|
|
idempotency_key: string
|
|
|
|
@DbAwareColumn({ type: "jsonb", nullable: true })
|
|
context: Record<string, unknown>
|
|
|
|
@DbAwareColumn({ type: "jsonb", nullable: true })
|
|
metadata: Record<string, unknown>
|
|
|
|
@FeatureFlagColumn("sales_channels", { type: "varchar", nullable: true })
|
|
sales_channel_id: string | null
|
|
|
|
@FeatureFlagDecorators("sales_channels", [
|
|
ManyToOne(() => SalesChannel),
|
|
JoinColumn({ name: "sales_channel_id" }),
|
|
])
|
|
sales_channel: SalesChannel
|
|
|
|
shipping_total?: number
|
|
discount_total?: number
|
|
tax_total?: number | null
|
|
refunded_total?: number
|
|
total?: number
|
|
subtotal?: number
|
|
refundable_amount?: number
|
|
gift_card_total?: number
|
|
gift_card_tax_total?: number
|
|
|
|
@AfterLoad()
|
|
private afterLoad(): void {
|
|
if (this.payment_sessions) {
|
|
this.payment_session = this.payment_sessions.find((p) => p.is_selected)!
|
|
}
|
|
}
|
|
|
|
@BeforeInsert()
|
|
private beforeInsert(): void {
|
|
this.id = generateEntityId(this.id, "cart")
|
|
}
|
|
}
|