feat: logique diff Sheets/Shopify (TDD)

Implémentation de la fonction pure computeDiff pour calculer les changements
entre les produits Google Sheets et Shopify, avec distinction des opérations
(create, update, deactivate, unchanged) et traçabilité des champs modifiés.

- Matching par handle (ref normalisée)
- Comparaison des champs: title, description, price, status
- Draft products Shopify non synchronisés ne sont pas déactivés
- Tests TDD: 9 cas couverts (empty, create, unchanged, update, deactivate)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 20:51:21 +02:00
parent f415138103
commit 2988ecab0b
2 changed files with 162 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
import { computeDiff } from '@/lib/syncDiff'
import type { SyncProduct, ShopifyProductFull } from '@/types/sync'
const sheet = (ref: string, overrides: Partial<SyncProduct> = {}): SyncProduct => ({
ref,
handle: ref.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''),
title: `Produit ${ref}`,
description: 'Desc',
price: '10.00',
stock: 5,
status: 'active',
...overrides,
})
const shopify = (ref: string, shopifyId = '111', overrides: Partial<ShopifyProductFull> = {}): ShopifyProductFull => ({
shopifyId,
...sheet(ref, overrides),
...overrides,
})
describe('computeDiff', () => {
it('retourne vide si aucun produit des deux côtés', () => {
const { changes, summary } = computeDiff([], [])
expect(changes).toHaveLength(0)
expect(summary).toEqual({ create: 0, update: 0, deactivate: 0, unchanged: 0 })
})
it('marque comme create si produit présent dans Sheets mais absent de Shopify', () => {
const { changes, summary } = computeDiff([sheet('REF-1')], [])
expect(changes).toHaveLength(1)
expect(changes[0].type).toBe('create')
expect(changes[0].ref).toBe('REF-1')
expect(summary.create).toBe(1)
})
it('marque comme unchanged si produits identiques', () => {
const s = sheet('REF-2')
const sp = shopify('REF-2')
const { changes, summary } = computeDiff([s], [sp])
expect(changes[0].type).toBe('unchanged')
expect(summary.unchanged).toBe(1)
})
it('marque comme update si le titre diffère', () => {
const s = sheet('REF-3', { title: 'Nouveau titre' })
const sp = shopify('REF-3', '222', { title: 'Ancien titre' })
const { changes } = computeDiff([s], [sp])
expect(changes[0].type).toBe('update')
expect(changes[0].changedFields).toContainEqual({
field: 'title',
from: 'Ancien titre',
to: 'Nouveau titre',
})
})
it('marque comme update si le prix diffère', () => {
const s = sheet('REF-4', { price: '25.00' })
const sp = shopify('REF-4', '333', { price: '20.00' })
const { changes } = computeDiff([s], [sp])
expect(changes[0].type).toBe('update')
expect(changes[0].changedFields).toContainEqual({ field: 'price', from: '20.00', to: '25.00' })
})
it('marque comme update si le statut diffère', () => {
const s = sheet('REF-5', { status: 'draft' })
const sp = shopify('REF-5', '444', { status: 'active' })
const { changes } = computeDiff([s], [sp])
expect(changes[0].type).toBe('update')
})
it('marque comme deactivate si produit ACTIF dans Shopify absent des Sheets', () => {
const sp = shopify('REF-6', '555', { status: 'active' })
const { changes, summary } = computeDiff([], [sp])
expect(changes[0].type).toBe('deactivate')
expect(summary.deactivate).toBe(1)
})
it("ne marque PAS comme deactivate si le produit Shopify est déjà draft", () => {
const sp = shopify('REF-7', '666', { status: 'draft' })
const { changes } = computeDiff([], [sp])
expect(changes).toHaveLength(0)
})
it('calcule le résumé correctement sur un mix de changements', () => {
const sheetProducts = [sheet('REF-A'), sheet('REF-B', { title: 'Modifié' }), sheet('REF-C')]
const shopifyProducts = [
shopify('REF-B', '10', { title: 'Original' }),
shopify('REF-C', '11'),
shopify('REF-D', '12', { status: 'active' }),
]
const { summary } = computeDiff(sheetProducts, shopifyProducts)
expect(summary.create).toBe(1) // REF-A
expect(summary.update).toBe(1) // REF-B
expect(summary.unchanged).toBe(1) // REF-C
expect(summary.deactivate).toBe(1) // REF-D
})
})