diff --git a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-desktop-after.png b/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-desktop-after.png new file mode 100644 index 00000000..7eb9eeaf Binary files /dev/null and b/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-desktop-after.png differ diff --git a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-desktop-before.png b/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-desktop-before.png new file mode 100644 index 00000000..1696acb5 Binary files /dev/null and b/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-desktop-before.png differ diff --git a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-mobile-after.png b/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-mobile-after.png new file mode 100644 index 00000000..74e4e9b3 Binary files /dev/null and b/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-mobile-after.png differ diff --git a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-mobile-before.png b/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-mobile-before.png new file mode 100644 index 00000000..afa566f7 Binary files /dev/null and b/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-mobile-before.png differ diff --git a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-tablet-after.png b/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-tablet-after.png new file mode 100644 index 00000000..30d5ba5f Binary files /dev/null and b/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-tablet-after.png differ diff --git a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-tablet-before.png b/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-tablet-before.png new file mode 100644 index 00000000..fb7a703a Binary files /dev/null and b/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-tablet-before.png differ diff --git a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/visual-evidence-manifest.json b/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/visual-evidence-manifest.json new file mode 100644 index 00000000..8a3cbdde --- /dev/null +++ b/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/visual-evidence-manifest.json @@ -0,0 +1,49 @@ +{ + "kind": "VisualEvidenceManifestV1", + "taskId": "workboard-94209138-31f6-422e-ac8c-181ad391b8a7", + "view": "POS extra sale audit", + "files": [ + { + "device": "mobile", + "state": "before", + "path": "docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-mobile-before.png", + "width": 390, + "height": 844 + }, + { + "device": "mobile", + "state": "after", + "path": "docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-mobile-after.png", + "width": 390, + "height": 844 + }, + { + "device": "tablet", + "state": "before", + "path": "docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-tablet-before.png", + "width": 768, + "height": 1024 + }, + { + "device": "tablet", + "state": "after", + "path": "docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-tablet-after.png", + "width": 768, + "height": 1024 + }, + { + "device": "desktop", + "state": "before", + "path": "docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-desktop-before.png", + "width": 1440, + "height": 900 + }, + { + "device": "desktop", + "state": "after", + "path": "docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-desktop-after.png", + "width": 1440, + "height": 900 + } + ] +} diff --git a/src/components/displays/department/pos/order/PosOrderItemEditModal.vue b/src/components/displays/department/pos/order/PosOrderItemEditModal.vue index e0cc3301..3e195a92 100644 --- a/src/components/displays/department/pos/order/PosOrderItemEditModal.vue +++ b/src/components/displays/department/pos/order/PosOrderItemEditModal.vue @@ -3,6 +3,11 @@ import { computed, reactive, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { SessionUser } from "@/components/session/token/SessionUser.vue"; import { buildAuditedOrderItemReasonPayload, editOrderItem } from "@/components/shop/OrdersItems.vue"; +import { + EXTRA_SALE_REASONS, + isExtraSaleAuditProduct, + isExtraSaleCommentRequired, +} from "@/components/displays/department/pos/utils/extraSaleAudit.js"; const props = defineProps({ modelValue: { @@ -29,6 +34,8 @@ const form = reactive({ notes: '', reference: '', quantity: '1', + extraSaleReasonCode: '', + extraSaleComment: '', }); const isSubmitting = ref(false); @@ -45,6 +52,8 @@ const syncForm = () => { form.notes = String(props.orderItem?.notes ?? ''); form.reference = String(props.orderItem?.reference ?? ''); form.quantity = String(props.orderItem?.quantity ?? 1); + form.extraSaleReasonCode = String(props.orderItem?.extra_sale_reason_code ?? ''); + form.extraSaleComment = String(props.orderItem?.extra_sale_comment ?? ''); errorMessage.value = ''; }; @@ -60,8 +69,27 @@ const isQuantityValid = computed(() => { return Number.isInteger(value) && value > 0; }); +const requiresExtraSaleAudit = computed(() => isExtraSaleAuditProduct(props.orderItem?.product || props.orderItem)); + +const isExtraSaleAuditValid = computed(() => { + if (!requiresExtraSaleAudit.value) { + return true; + } + + if (!form.extraSaleReasonCode) { + return false; + } + + return !isExtraSaleCommentRequired(form.extraSaleReasonCode) || form.extraSaleComment.trim().length > 0; +}); + const canSubmit = computed(() => { - return Boolean(props.orderItem) && props.canEdit && !isSubmitting.value && isPriceValid.value && isQuantityValid.value; + return Boolean(props.orderItem) + && props.canEdit + && !isSubmitting.value + && isPriceValid.value + && isQuantityValid.value + && isExtraSaleAuditValid.value; }); const closeModal = () => { @@ -90,7 +118,11 @@ const saveChanges = async () => { reason_code: props.orderItem.reason_code, reason_label_snapshot: props.orderItem.reason_label_snapshot, reason_comment: form.notes, - }) + }), + { + extra_sale_reason_code: form.extraSaleReasonCode || null, + extra_sale_comment: form.extraSaleComment.trim() || null, + } ); emits('saved'); } catch (error) { @@ -177,6 +209,41 @@ const saveChanges = async () => { > + +
{ + if (!product || typeof product !== "object") { + return false; + } + + if (product.requires_extra_sale_audit === true) { + return true; + } + + if (Number(product.id ?? product.product_id ?? 0) === EXTRA_SALE_PRODUCT_ID) { + return true; + } + + return String(product.name ?? "").trim() === EXTRA_SALE_PRODUCT_NAME; +}; + +export const isExtraSaleCommentRequired = (reasonCode) => { + return EXTRA_SALE_REASONS.some((reason) => reason.code === reasonCode && reason.commentRequired); +}; + +export const extraSaleAuditPayload = ({ reasonCode = null, comment = null } = {}) => ({ + extra_sale_reason_code: String(reasonCode || "").trim() || null, + extra_sale_comment: String(comment || "").trim() || null, +}); + +export const getExtraSaleAuditFromOrderItem = (orderItem = {}) => + extraSaleAuditPayload({ + reasonCode: orderItem.extra_sale_reason_code, + comment: orderItem.extra_sale_comment, + }); + +export const promptExtraSaleAuditIfRequired = async (product, initial = {}) => { + if (!isExtraSaleAuditProduct(product)) { + return extraSaleAuditPayload(initial); + } + + const initialReason = String(initial.reasonCode ?? initial.extra_sale_reason_code ?? "").trim(); + const initialComment = String(initial.comment ?? initial.extra_sale_comment ?? "").trim(); + const optionsMarkup = EXTRA_SALE_REASONS.map((reason) => { + const selected = reason.code === initialReason ? " selected" : ""; + return ``; + }).join(""); + + const result = await Swal.fire({ + title: "Godkend 10 min ekstra", + html: ` + +
+ +
+ + + `, + showCancelButton: true, + confirmButtonText: "Godkend", + focusConfirm: false, + preConfirm: () => { + const reasonCode = document.getElementById("extra-sale-reason-code")?.value || ""; + const comment = document.getElementById("extra-sale-comment")?.value || ""; + + if (!reasonCode) { + Swal.showValidationMessage("Vælg en godkendt årsag"); + return false; + } + + if (isExtraSaleCommentRequired(reasonCode) && !String(comment || "").trim()) { + Swal.showValidationMessage("Kommentar er påkrævet for denne årsag"); + return false; + } + + return extraSaleAuditPayload({ reasonCode, comment }); + }, + }); + + return result.isConfirmed ? result.value : null; +}; diff --git a/src/components/forms/department/pos/SelectProductsFormPOS.vue b/src/components/forms/department/pos/SelectProductsFormPOS.vue index 596112ac..51296d9b 100644 --- a/src/components/forms/department/pos/SelectProductsFormPOS.vue +++ b/src/components/forms/department/pos/SelectProductsFormPOS.vue @@ -30,6 +30,10 @@ import WhiteBox from "@/components/displays/boxes/WhiteBox.vue"; import ProductBox from "@/components/displays/boxes/ProductBox.vue"; import { addOrderItemAddons } from "@/components/displays/department/pos/utils/addOrderItemAddons.js"; import { getPicture } from "@/components/displays/department/pos/displays/Piktogrammer.vue"; +import { + getExtraSaleAuditFromOrderItem, + promptExtraSaleAuditIfRequired, +} from "@/components/displays/department/pos/utils/extraSaleAudit.js"; const emits = defineEmits(['onAddToCartProduct', 'onAddProduct', 'onSelectProduct', 'onSelectionInvalidated']); const route = useRoute(); @@ -386,10 +390,15 @@ const warnIfRestrictedAddonsWereSkipped = async (restrictedSelections = []) => { await showRestrictionWarning("pos.restrictions.restricted_items_removed"); }; +const resolveExtraSaleAudit = async (product, initial = {}) => { + const audit = await promptExtraSaleAuditIfRequired(product, initial); + return audit === null ? null : audit; +}; -const showAddMultipleProducts = (productId) => { + +const showAddMultipleProducts = async (productId) => { const product = findProductById(productId); const restriction = getScopedProductRestriction(product); if (restriction.restricted) { @@ -397,7 +406,7 @@ const showAddMultipleProducts = (productId) => { return; } - Swal.fire({ + const quantityResult = await Swal.fire({ title: 'Tilføj flere produkter', input: 'number', inputAttributes: { @@ -405,7 +414,7 @@ const showAddMultipleProducts = (productId) => { }, showCancelButton: true, confirmButtonText: 'Tilføj', - showLoaderOnConfirm: true, + showLoaderOnConfirm: false, preConfirm: (inputValue) => { // Check if the input number is higher than 200 if (inputValue > 200) { @@ -423,17 +432,32 @@ const showAddMultipleProducts = (productId) => { Swal.showValidationMessage('Order ID is required'); return false; } - return createOrderItem(orderId, productId, inputValue) - .then(async (result) => { - const createdItemId = Number(result?.data?.data?.id ?? 0); - await addAddonsToOrderMiddleware(productId, inputValue, createdItemId, orderId); - await loadOrderItems(); - }) - .catch((error) => handleCreateOrderItemError(error, { validationMessage: true }).then(() => false)); + return Number(inputValue); } }, allowOutsideClick: () => !Swal.isLoading() }); + if (!quantityResult.isConfirmed) { + return; + } + + const orderId = getValidOrderId(); + if (!orderId) { + return; + } + + const audit = await resolveExtraSaleAudit(product); + if (audit === null) { + return; + } + + await createOrderItem(orderId, productId, quantityResult.value, null, null, null, audit) + .then(async (result) => { + const createdItemId = Number(result?.data?.data?.id ?? 0); + await addAddonsToOrderMiddleware(productId, quantityResult.value, createdItemId, orderId); + await loadOrderItems(); + }) + .catch((error) => handleCreateOrderItemError(error)); }; const EXTRAORDINARY_CHEMISTRY_PRODUCT_NAME = "Ekstraordinær pr. 10 min inkl. kemi"; @@ -685,7 +709,7 @@ const addProductWithAddonsToOrder = async (product_id) => { // Check if the product requires a note if (productRequiresOrderItemNote(product)) { // Show the note input - await Swal.fire({ + const noteResult = await Swal.fire({ title: 'Tilføj en note', input: 'text', inputLabel: 'Noten kan ses af kunden. F.eks. "Fjernelse af graffiti på venstre side"', @@ -694,35 +718,32 @@ const addProductWithAddonsToOrder = async (product_id) => { }, showCancelButton: true, confirmButtonText: 'Tilføj', - showLoaderOnConfirm: true, + showLoaderOnConfirm: false, inputValidator: (note) => { if (!String(note || '').trim()) { return 'Note er påkrævet for dette produkt'; } return null; }, - preConfirm: (note) => { - const normalizedNote = String(note || '').trim(); - const confirmedOrderId = getValidOrderId(); - if (!confirmedOrderId) { - Swal.showValidationMessage('Order ID is required'); - return false; - } - // Show the fake create order item - showPendingCreateOrderItem(product, 1, getUserProductPrice(product), 0, normalizedNote); - // Create the order item - return createOrderItem(confirmedOrderId, product_id, 1, 0, normalizedNote) - .then(async (result) => { - let order_item_id = result.data.data.id; - // Add the addons to the order - await addAddonsToOrderMiddleware(product_id, 1, order_item_id, confirmedOrderId).then(() => { - loadOrderItems(); - }); - }) - .catch((error) => handleCreateOrderItemError(error, { validationMessage: true }).then(() => false)); - }, - allowOutsideClick: () => !Swal.isLoading() + preConfirm: (note) => String(note || '').trim(), }); + if (!noteResult.isConfirmed) { + return; + } + const normalizedNote = noteResult.value; + const audit = await resolveExtraSaleAudit(product); + if (audit === null) { + return; + } + showPendingCreateOrderItem(product, 1, getUserProductPrice(product), 0, normalizedNote); + await createOrderItem(orderId, product_id, 1, 0, normalizedNote, null, audit) + .then(async (result) => { + let order_item_id = result.data.data.id; + await addAddonsToOrderMiddleware(product_id, 1, order_item_id, orderId).then(() => { + loadOrderItems(); + }); + }) + .catch((error) => handleCreateOrderItemError(error)); return; } // If the product requires a note, show the note input @@ -739,7 +760,12 @@ const addProductWithAddonsToOrder = async (product_id) => { return; } // Create the order item - await createOrderItem(orderId, product_id, quantity) + const audit = await resolveExtraSaleAudit(product); + if (audit === null) { + clearPendingOrderItems(); + return; + } + await createOrderItem(orderId, product_id, quantity, null, null, null, audit) .then(async (result) => { let order_item_id = result.data.data.id; // Add the addons to the order @@ -816,12 +842,19 @@ const addPreviousOrderToCurrent = async (previousOrder) => { const discountedPrice = String(getUserProductPrice({ ...previousOrderProduct, price: basePrice })); showPendingCreateOrderItem({ ...previousOrderProduct, price: basePrice }, quantity, discountedPrice); + const audit = await resolveExtraSaleAudit(previousOrderProduct, getExtraSaleAuditFromOrderItem(orderItem)); + if (audit === null) { + clearPendingOrderItems(); + continue; + } const result = await createOrderItem( orderId, productId, quantity, null, - String(orderItem?.notes ?? "") + String(orderItem?.notes ?? ""), + null, + audit ).catch((error) => handleCreateOrderItemError(error)); const sourceItemId = getPreviousOrderItemId(orderItem); const createdItemId = Number(result?.data?.data?.id ?? 0); @@ -854,12 +887,19 @@ const addPreviousOrderToCurrent = async (previousOrder) => { String(getUserProductPrice(previousOrderProduct)), relatedItemId ); + const audit = await resolveExtraSaleAudit(previousOrderProduct, getExtraSaleAuditFromOrderItem(orderItem)); + if (audit === null) { + clearPendingOrderItems(); + continue; + } await createOrderItem( orderId, productId, quantity, relatedItemId, - String(orderItem?.notes ?? "") + String(orderItem?.notes ?? ""), + null, + audit ).catch((error) => handleCreateOrderItemError(error)); } @@ -923,7 +963,12 @@ const addRecommendedProductToOrder = async (productId) => { } showPendingCreateOrderItem(product, 1, getRecommendedProductPrice(productId)); - const result = await createOrderItem(orderId, productId, 1).catch((error) => handleCreateOrderItemError(error)); + const audit = await resolveExtraSaleAudit(product); + if (audit === null) { + clearPendingOrderItems(); + return; + } + const result = await createOrderItem(orderId, productId, 1, null, null, null, audit).catch((error) => handleCreateOrderItemError(error)); const createdItemId = Number(result?.data?.data?.id ?? 0); if (createdItemId > 0) { await addAddonsToOrderMiddleware(productId, 1, createdItemId, orderId); diff --git a/src/components/shop/OrdersItems.vue b/src/components/shop/OrdersItems.vue index b7ba9f6d..5872fd9b 100644 --- a/src/components/shop/OrdersItems.vue +++ b/src/components/shop/OrdersItems.vue @@ -56,7 +56,16 @@ export const getOrderItems = (order_id) => { }); }; -export const createOrderItem = (order_id, product_id, quantity, related_item_id = null, notes = null, forcePrice = null, reasonData = null) => { +export const createOrderItem = ( + order_id, + product_id, + quantity, + related_item_id = null, + notes = null, + forcePrice = null, + reasonData = null, + extraSaleAudit = {} +) => { const token = localStorage.getItem('token'); if (!token) { return null; @@ -72,6 +81,7 @@ export const createOrderItem = (order_id, product_id, quantity, related_item_id related_item_id, notes, ...buildAuditedOrderItemReasonPayload(product_id, notes, reasonData), + ...extraSaleAudit }; if (forcePrice !== null && forcePrice !== undefined) { payload.price = forcePrice; @@ -95,7 +105,7 @@ export const removeOrderItem = (id) => { }); }; -export const editOrderItem = (id, price, notes, reference, quantity, reasonData = null) => { +export const editOrderItem = (id, price, notes, reference, quantity, reasonData = null, extraSaleAudit = {}) => { const token = localStorage.getItem('token'); if (!token) { return null; @@ -107,6 +117,7 @@ export const editOrderItem = (id, price, notes, reference, quantity, reasonData reference, quantity, ...(reasonData && typeof reasonData === "object" ? reasonData : {}), + ...extraSaleAudit }, { headers: { Authorization: `Bearer ${token}` diff --git a/src/components/shop/POSDepartmentProcess.vue b/src/components/shop/POSDepartmentProcess.vue index 076c3308..87fe49b6 100644 --- a/src/components/shop/POSDepartmentProcess.vue +++ b/src/components/shop/POSDepartmentProcess.vue @@ -10,6 +10,10 @@ import { getAttributes } from "@/components/shop/CustomerAttributes.vue"; import { SessionUser } from "@/components/session/token/SessionUser.vue"; import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue"; import { doesOrderContainWashCertificateProduct } from "@/components/displays/department/pos/utils/washCertificate.js"; +import { + getExtraSaleAuditFromOrderItem, + promptExtraSaleAuditIfRequired, +} from "@/components/displays/department/pos/utils/extraSaleAudit.js"; import { getCustomerProductRestriction, getProductCategoryRestrictionForCustomer, @@ -1572,12 +1576,19 @@ const copyLastWashReferenceToEmptyCurrentOrder = async (sourceReference) => { } }; -const createCopiedOrderItem = (targetOrderId, sourceItem, relatedItemId = null) => { +const createCopiedOrderItem = async (targetOrderId, sourceItem, relatedItemId = null) => { const productId = getOrderItemProductId(sourceItem); const quantity = getOrderItemQuantity(sourceItem); if (!productId || !quantity) { return Promise.resolve(null); } + const audit = await promptExtraSaleAuditIfRequired( + sourceItem?.product || { id: productId }, + getExtraSaleAuditFromOrderItem(sourceItem) + ); + if (audit === null) { + return Promise.resolve(null); + } // Preserve audited-order-item reason metadata from the source row so the // copied POST satisfies the server-side `reason_comment` requirement for @@ -1600,7 +1611,8 @@ const createCopiedOrderItem = (targetOrderId, sourceItem, relatedItemId = null) relatedItemId, getOrderItemNotes(sourceItem), null, - sourceReasonData + sourceReasonData, + audit ); }; @@ -2627,13 +2639,19 @@ export const hydrateSelectedOrderBookingForDesktop = async () => { } const forcedPrimaryPrice = primaryProduct.price ?? null; + const primaryAudit = await promptExtraSaleAuditIfRequired(primaryProduct); + if (primaryAudit === null) { + return false; + } const primaryItemResponse = await createOrderItem( normalizedOrderId, primaryProduct.id, 1, null, null, - forcedPrimaryPrice + forcedPrimaryPrice, + null, + primaryAudit ); const relatedPrimaryItemId = toPositiveInteger(primaryItemResponse?.data?.data?.id); @@ -2645,6 +2663,10 @@ export const hydrateSelectedOrderBookingForDesktop = async () => { } const secondaryProduct = await fetchOrderBookingProductWithPricing(secondaryProductId); + const secondaryAudit = await promptExtraSaleAuditIfRequired(secondaryProduct || { id: secondaryProductId }); + if (secondaryAudit === null) { + return false; + } await createOrderItem( normalizedOrderId, @@ -2652,7 +2674,9 @@ export const hydrateSelectedOrderBookingForDesktop = async () => { Math.max(1, Number.parseInt(String(bookingItem?.quantity ?? 1), 10) || 1), relatedPrimaryItemId, String(bookingItem?.notes ?? "").trim() || null, - secondaryProduct?.price ?? null + secondaryProduct?.price ?? null, + null, + secondaryAudit ); } diff --git a/tests/unit/pos-department-process.spec.js b/tests/unit/pos-department-process.spec.js index a1814281..a493a459 100644 --- a/tests/unit/pos-department-process.spec.js +++ b/tests/unit/pos-department-process.spec.js @@ -777,8 +777,14 @@ describe("POSDepartmentProcess.hydrateSelectedOrderBookingForDesktop", () => { await expect(hydrateSelectedOrderBookingForDesktop()).resolves.toBe(true); - expect(mocks.createOrderItem).toHaveBeenNthCalledWith(1, 51207, 10, 1, null, null, 500); - expect(mocks.createOrderItem).toHaveBeenNthCalledWith(2, 51207, 20, 3, 9001, "Addon note", 500); + expect(mocks.createOrderItem).toHaveBeenNthCalledWith(1, 51207, 10, 1, null, null, 500, null, { + extra_sale_comment: null, + extra_sale_reason_code: null, + }); + expect(mocks.createOrderItem).toHaveBeenNthCalledWith(2, 51207, 20, 3, 9001, "Addon note", 500, null, { + extra_sale_comment: null, + extra_sale_reason_code: null, + }); expect(SessionUser.objects.products.get.single).toHaveBeenCalledWith(10, { department_id: 2, customer_id: 12345679,