Quick summary
Merging two contacts silently drops a date custom field that only the secondary record has. The merge is supposed to patch empty fields on the main record from the secondary one, and it does that for every other field type. Date custom fields go missing without any error or log entry.
This is the same double normalisation as #21, reached without going anywhere near the API. It's worth recording separately because a fix confined to the API layer, like #27, doesn't reach it.
Steps to reproduce
- In Jetpack CRM → Settings → Custom Fields, add a Contact custom field of type Date (e.g. label
Contract Date, slug contract-date).
- Create contact A and leave the date custom field empty.
- Create contact B and set the date custom field to a real date.
- Merge B into A, with A as the main record.
Expected: A ends up with B's date, the same as any other field the main record was missing. The merge log says "Copied field ... from secondary record over main record, (main was empty)."
Actual: A's date custom field is still empty. The merge reports the copy as a change, so the log claims it happened.
Root cause
zeroBSCRM_mergeCustomers() loads the main record with zeroBS_getCustomer(), which returns date custom fields as the raw UTS the DAL stores:
$master = zeroBS_getCustomer( $dominantID ); // includes/ZeroBSCRM.DAL3.Helpers.php:1303
$masterNewMeta = $master; // :1343
The patch loop then copies the secondary record's value in, also a raw UTS:
$masterNewMeta[ $fieldPrefix . $fieldKey ] = $slave[ $fieldKey ]; // :1362
and the whole array goes to zeroBS_addUpdateCustomer() at :1471, which normalises it again with $removeEmpties on:
$zbsCustomerMeta = zeroBS_buildContactMeta( $cFields, $existingMeta, $metaBuilderPrefix, '', true ); // :1863
The date branch of the builder expects Y-m-d, gets "1784764800", and DateTime::createFromFormat( '!Y-m-d', '1784764800' ) fails. jpcrm_date_str_to_uts() returns false, $removeEmpties drops the key at :3049, and addUpdateContact() skips absent custom fields at ZeroBSCRM.DAL3.Obj.Contacts.php:3087. Nothing is written and nothing complains.
Three callers hand the builder an array that has already been through it: api/create_customer.php:51, api/create_company.php:30, and this one. Everything else normalises once and goes straight to addUpdateContact().
Fixed by #25
The idempotency fix in #25 covers this, because it makes the date branch pass an already-converted timestamp through untouched. Verified both ways:
Trunk: ✘ Merging contacts copies a date custom field from the secondary record.
Failed asserting that '' matches expected 1784764800.
Branch: OK (1 test, 2 assertions)
Filing it anyway so the case is recorded, since it's the clearest argument that the builder is the right place to fix this rather than the API endpoints.
The regression test is blocked
I've written the test but left it out of #25. It trips a pre-existing deprecation, and phpunit.11.xml.dist sets failOnDeprecation="true", so the suite exits 1 with it in:
ZeroBSCRM.DAL3.ObjectLayer.php:934
strlen(): Passing null to parameter #1 ($string) of type string is deprecated
Tests: 53, Assertions: 222, Deprecations: 1
The null values are tw, fb and li. A contact created without socials has NULL in those columns, getContact() hands the nulls back, and the merge passes them straight into the max-length check:
if ( strlen( $val ) > $this->objectModel[ $fieldKey ]['max_len'] ) {
It fires identically on trunk and on the #25 branch, so it has nothing to do with dates. Setting the socials in the test fixture doesn't avoid it, the nulls originate inside the merge.
Two ways to unblock the test, and I don't have a strong view on which:
- Cast at the check,
strlen( (string) $val ). One line, and it fixes every other caller that round-trips a contact through addUpdateContact().
- Stop the merge handing null socials down in the first place, which is the real bug but a bigger change.
The test itself is ready and I can attach it to whichever gets picked.
Related
Quick summary
Merging two contacts silently drops a date custom field that only the secondary record has. The merge is supposed to patch empty fields on the main record from the secondary one, and it does that for every other field type. Date custom fields go missing without any error or log entry.
This is the same double normalisation as #21, reached without going anywhere near the API. It's worth recording separately because a fix confined to the API layer, like #27, doesn't reach it.
Steps to reproduce
Contract Date, slugcontract-date).Expected: A ends up with B's date, the same as any other field the main record was missing. The merge log says "Copied field ... from secondary record over main record, (main was empty)."
Actual: A's date custom field is still empty. The merge reports the copy as a change, so the log claims it happened.
Root cause
zeroBSCRM_mergeCustomers()loads the main record withzeroBS_getCustomer(), which returns date custom fields as the raw UTS the DAL stores:The patch loop then copies the secondary record's value in, also a raw UTS:
and the whole array goes to
zeroBS_addUpdateCustomer()at:1471, which normalises it again with$removeEmptieson:The
datebranch of the builder expectsY-m-d, gets"1784764800", andDateTime::createFromFormat( '!Y-m-d', '1784764800' )fails.jpcrm_date_str_to_uts()returnsfalse,$removeEmptiesdrops the key at:3049, andaddUpdateContact()skips absent custom fields atZeroBSCRM.DAL3.Obj.Contacts.php:3087. Nothing is written and nothing complains.Three callers hand the builder an array that has already been through it:
api/create_customer.php:51,api/create_company.php:30, and this one. Everything else normalises once and goes straight toaddUpdateContact().Fixed by #25
The idempotency fix in #25 covers this, because it makes the
datebranch pass an already-converted timestamp through untouched. Verified both ways:Filing it anyway so the case is recorded, since it's the clearest argument that the builder is the right place to fix this rather than the API endpoints.
The regression test is blocked
I've written the test but left it out of #25. It trips a pre-existing deprecation, and
phpunit.11.xml.distsetsfailOnDeprecation="true", so the suite exits 1 with it in:The null values are
tw,fbandli. A contact created without socials has NULL in those columns,getContact()hands the nulls back, and the merge passes them straight into the max-length check:It fires identically on trunk and on the #25 branch, so it has nothing to do with dates. Setting the socials in the test fixture doesn't avoid it, the nulls originate inside the merge.
Two ways to unblock the test, and I don't have a strong view on which:
strlen( (string) $val ). One line, and it fixes every other caller that round-trips a contact throughaddUpdateContact().The test itself is ready and I can attach it to whichever gets picked.
Related
create_customer.phprework