diff --git a/src/clj/auto_ap/jobs/rekey_square_external_ids.clj b/src/clj/auto_ap/jobs/rekey_square_external_ids.clj index 2fc0c10f..2203c9b8 100644 --- a/src/clj/auto_ap/jobs/rekey_square_external_ids.clj +++ b/src/clj/auto_ap/jobs/rekey_square_external_ids.clj @@ -115,6 +115,28 @@ [:charge/type-name :charge/total :charge/tip :charge/tax :charge/date :charge/processor :charge/note :charge/reference-link]) +(defn- raw-square-id + "The Square id inside a charge's external id, with any client scoping removed. + + Stripping only the `square/charge/` prefix is not enough. Once a charge has been scoped to one + client, a second order processing the same charge would read `NGCC-CC-` as the id and scope + it again, producing `square/charge/NGCD-CD-NGCC-CC-`. The importer then computes the + correct single-scoped key, fails to find it, and creates a second charge — silently doubling + the tender. + + Client codes may themselves contain dashes, so the scope cannot be recognised by pattern. It is + recovered from the entity instead: whoever the charge currently belongs to is exactly whose + scope its key carries." + [db charge old] + (let [ent (dc/entity db charge) + code (:client/code (:charge/client ent)) + loc (:charge/location ent) + owner-prefix (when (and code loc) (str charge-prefix code "-" loc "-"))] + (cond + (and owner-prefix (.startsWith ^String old ^String owner-prefix)) (subs old (count owner-prefix)) + (.startsWith ^String old charge-prefix) (subs old (count charge-prefix)) + :else old))) + (defn- charge-plan-for-order "What one order needs doing to its charges: `:keep` where the charge is already this order's alone, `:clone` where it is shared with another order and this order needs its own copy." @@ -127,9 +149,9 @@ :let [charge (:v d) old (:v (first (dc/datoms db :eavt charge :charge/external-id)))] :when old] - (let [raw (if (.startsWith ^String old charge-prefix) (subs old (count charge-prefix)) old) + (let [raw (or (get @seen charge) (raw-square-id db charge old)) new-key (str charge-prefix code "-" loc "-" raw)] - {:order order-eid :charge charge :new-key new-key + {:order order-eid :charge charge :new-key new-key :raw raw :client (:db/id (:sales-order/client oe)) :location loc :action (if (contains? @seen charge) :clone :keep)}))))) @@ -150,7 +172,7 @@ `orders` is the collection of order entity ids to process — typically every order belonging to the clients that share, or have ever shared, a Square location." [orders batch-size] - (let [seen (atom #{}) + (let [seen (atom {}) cloned (atom 0) rekeyed (atom 0)] (doseq [batch (partition-all batch-size orders)] @@ -160,7 +182,7 @@ plan (charge-plan-for-order db o seen) :let [{:keys [charge new-key action client location]} plan] tx-item (if (= :keep action) - (do (swap! seen conj charge) + (do (swap! seen assoc charge (:raw plan)) (swap! rekeyed inc) [{:db/id charge :charge/external-id new-key}]) (let [ent (dc/entity db charge) diff --git a/test/clj/auto_ap/jobs/rekey_square_external_ids_test.clj b/test/clj/auto_ap/jobs/rekey_square_external_ids_test.clj index d79e0a81..78a459e4 100644 --- a/test/clj/auto_ap/jobs/rekey_square_external_ids_test.clj +++ b/test/clj/auto_ap/jobs/rekey_square_external_ids_test.clj @@ -3,6 +3,7 @@ [auto-ap.datomic :refer [conn]] [auto-ap.integration.util :refer [setup-test-data wrap-setup]] [auto-ap.jobs.rekey-square-external-ids :as sut] + [clojure.string] [clojure.test :refer [deftest is testing use-fixtures]] [datomic.api :as dc])) @@ -106,3 +107,18 @@ (is (= {:rekeyed 2 :cloned 0} (sut/split-and-rekey-charges! [order-a order-b] 100)) "each order now owns its charge outright, so both are simple re-keys") (is (= after-first (charge-count)) "and no further entities appear"))))) + +(deftest clone-is-not-double-scoped-across-batches + (testing "with a batch size of one, the second order sees a charge already carrying the first + client's scope. It must clone using the underlying Square id, not re-scope the scoped + key — otherwise the entity ends up keyed NGCD-CD-NGCC-CC-, the importer computes + the correct key, misses, and creates a second charge that doubles the tender." + (let [{:keys [order-a order-b code-a code-b]} (two-orders-sharing-one-charge)] + (sut/split-and-rekey-charges! [order-a order-b] 1) + (let [ka (:key (first (charges-of order-a))) + kb (:key (first (charges-of order-b)))] + (is (= (str "square/charge/" code-a "-CD-shared1") ka)) + (is (= (str "square/charge/" code-b "-CC-shared1") kb)) + (is (not (clojure.string/includes? kb (str code-a "-CD"))) + "the clone carries one scope, not two") + (is (= 2 (charge-count)))))))