diff --git a/README.md b/README.md index 7efa79c..bc9dba9 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ xml-disassembler parse | `--ignore-path ` | Path to the ignore file | .xmldisassemblerignore | | `--format ` | Output format: xml, json, json5, yaml | xml | | `--strategy ` | unique-id or grouped-by-tag | unique-id | +| `--multi-level ` | Further disassemble matching files: `file_pattern:root_to_strip:unique_id_elements` | (none) | #### Reassemble options @@ -169,6 +170,30 @@ xml-disassembler disassemble ./my.xml --strategy grouped-by-tag --format yaml Reassembly preserves element content and structure. +### Multi-level disassembly + +For advanced use cases (e.g. Salesforce Loyalty Program Setup metadata), you can further disassemble specific output files by stripping a root element and re-running disassembly with different unique-id elements. + +Use `--multi-level ` where the spec is: + +`file_pattern:root_to_strip:unique_id_elements` + +- **file_pattern** – Match XML files whose name or path contains this (e.g. `programProcesses` or `programProcesses-meta`). +- **root_to_strip** – Element to strip/unwrap: if it is the root, its inner content becomes the new document; if it is a child (e.g. `programProcesses` under `LoyaltyProgramSetup`), it is unwrapped so its inner content becomes the root’s direct children. +- **unique_id_elements** – Comma-separated element names for the second-level disassembly (e.g. `parameterName,ruleName`). + +Example (loyalty program): strip the child `programProcesses` in each process file so parameters/rules can be disassembled: + +```bash +xml-disassembler disassemble ./Cloud_Kicks_Inner_Circle.loyaltyProgramSetup-meta.xml \ + --unique-id-elements "fullName,name,processName" \ + --multi-level "programProcesses:programProcesses:parameterName,ruleName" +``` + +A `.multi_level.json` config is written in the disassembly root so **reassemble** automatically does inner-level reassembly first, wraps files with the original root, then reassembles the top level. No extra flags are needed for reassembly. + +**Caveat:** Multi-level reassembly removes disassembled directories after reassembling each level, even when you do not pass `--postpurge`. This is required so the next level can merge the reassembled XML files. Use version control (e.g. Git) to recover the tree if needed, or run reassembly only in a pipeline where these changes can be discarded. + ## Ignore file Exclude files or directories from disassembly using an ignore file (default: `.xmldisassemblerignore`). The Rust implementation uses the [ignore](https://crates.io/crates/ignore) crate with `.gitignore`-style syntax. diff --git a/fixtures/multi-level/Cloud_Kicks_Inner_Circle.loyaltyProgramSetup-meta.xml b/fixtures/multi-level/Cloud_Kicks_Inner_Circle.loyaltyProgramSetup-meta.xml new file mode 100644 index 0000000..c1b2aac --- /dev/null +++ b/fixtures/multi-level/Cloud_Kicks_Inner_Circle.loyaltyProgramSetup-meta.xml @@ -0,0 +1,5499 @@ + + + + + RealTime + Manual Points Adjustment + Manual Points Adjustments + TransactionJournal + Active + + Numeric + 0 + false + false + false + EA_PerAdjustmentRewardTracking + Variable + + + Numeric + 0 + false + false + false + EA_RewardsTracking + Variable + + + Text + false + false + false + EventType + Variable + + + Text + false + false + false + GeneratedOrderCode + Variable + + + Numeric + 0 + false + false + false + LineItemTotalVoucherFaceValue + Variable + + + Numeric + 0 + false + false + false + LineItemTotalVoucherToIssue + Variable + + + Numeric + 0 + false + false + false + PointsAdjusted + Variable + + + Numeric + 0 + false + false + false + PointsToDebitForVoucherIssue + Variable + + + Numeric + 0 + false + false + false + PositiveBalanceCurrency + Variable + + + Text + false + false + false + VoucherDefinitionName + Formula + {!TransactionJournal.Voucher_Definition_Name__c} + + + Numeric + 0 + false + false + false + VoucherFaceValue + Variable + + + Numeric + 0 + false + false + false + VoucherTargetPoints + Variable + + + + Issue Voucher by Definition Name + + Equals + VoucherDefinitionName + {!VoucherDefinitionName} + Parameter + + IssueVoucher + + + Update Journal VoucherDefinition to TBD + + Equals + _action + update + + + Equals + _entity + TransactionJournal + + + Equals + Id + {!TransactionJournal.Id} + + + Equals + Voucher_Definition_Name__c + "TBD" + + Crud + update + TransactionJournal + + + Update Journal VoucherDefinition to TBD for deletion + + Equals + _action + update + + + Equals + _entity + TransactionJournal + + + Equals + Id + {!TransactionJournal.Id} + + + Equals + Voucher_Definition_Name__c + "TBD" + + Crud + update + TransactionJournal + + + 1 AND 2 AND 3 + + NotEquals + 1 + TransactionJournal.Voucher_Definition_Name__c + '' + Formula + + + NotEquals + 2 + TransactionJournal.Voucher_Definition_Name__c + NULL + Formula + + + NotEquals + 3 + TransactionJournal.Voucher_Definition_Name__c + NonAdHoc + Literal + + IF VoucherDefinitonName is NOT BLANK and Not Equal 'NonAdHoc' + Condition + + + 1 + + Equals + 1 + TransactionJournal.Voucher_Definition_Name__c + NonAdHoc + Literal + + IF VoucherDefinitonName is NOT BLANK and EQUAL 'NonAdHoc' + Condition + + false + Bulk Voucher Upload + 2024-03-01 + Active + + IF VoucherDefinitonName is NOT BLANK and Not Equal 'NonAdHoc' + 1 + + + IF VoucherDefinitonName is NOT BLANK and EQUAL 'NonAdHoc' + 2 + + + Issue Voucher by Definition Name + IF VoucherDefinitonName is NOT BLANK and Not Equal 'NonAdHoc' + 1 + + + Update Journal VoucherDefinition to TBD + IF VoucherDefinitonName is NOT BLANK and Not Equal 'NonAdHoc' + 2 + + + Update Journal VoucherDefinition to TBD for deletion + IF VoucherDefinitonName is NOT BLANK and EQUAL 'NonAdHoc' + 1 + + + + + GET Positive Balance Currency + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsBalance + PositiveBalanceCurrency + + GetMemberPointBalance + + + Update Status Points + + Equals + _action + update + + + Equals + _entity + LoyaltyProgramMember + + + Equals + Id + {!TransactionJournal.MemberId} + + + Equals + Status_Points__c + {!PositiveBalanceCurrency} + + Crud + update + LoyaltyProgramMember + + false + Set Up Step + Finalize + 2024-03-01 + Active + + GET Positive Balance Currency + 1 + + + Update Status Points + 2 + + + + + Retrieve Points for Adjustment from Ledgers + + Equals + MemberId + {!TransactionJournal.MemberId} + + + Equals + JournalId + {!TransactionJournal.Id} + + + Equals + LedgerPoints + PointsAdjusted + + + Equals + EventType + EventType + + RunFlow + Loyalty_Process_Get_Points_for_Manual_Adjustments + + + GET Runtime VoucherDefinition Values + + Equals + VoucherRewardTargetPoints + VoucherTargetPoints + + + Equals + VoucherRewardFaceValue + VoucherFaceValue + + RunFlow + Loyalty_Process_Get_Runtime_Configuration + + + Credit Adjustment Points + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsToCredit + CEILING({!TransactionJournal.TransactionAmount} ) + + CreditPoints + + + Debit Adjustment Points + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsToDebit + ABS(CEILING({!TransactionJournal.TransactionAmount} )) + + DebitPoints + + + GET Positive Balance Currency [After Adjustment ] + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsBalance + PositiveBalanceCurrency + + GetMemberPointBalance + + + Assign Values for Vouchers Issuance + + Equals + PointsToDebitForVoucherIssue + 1 + FLOOR({!PositiveBalanceCurrency} / {!VoucherTargetPoints} )*{!VoucherTargetPoints} + + + Equals + LineItemTotalVoucherToIssue + 2 + FLOOR({!PositiveBalanceCurrency} / {!VoucherTargetPoints} ) + + + Equals + LineItemTotalVoucherFaceValue + 3 + FLOOR({!PositiveBalanceCurrency} / {!VoucherTargetPoints} )*{!VoucherFaceValue} + + AssignParameterValues + + + Generate OrderID Code + + Equals + VoucherDefinitionName + "Reward" + + + Equals + VoucherCount + 1 + + + Equals + VoucherCodesToGenerate + 1 + + + Equals + VoucherCodes + GeneratedOrderCode + + RunFlow + Loyalty_Process_Generate_Voucher_Codes + + + Update Journal OrderId field + + Equals + _action + update + + + Equals + _entity + TransactionJournal + + + Equals + Id + {!TransactionJournal.Id} + + + Equals + Order_ID__c + {!GeneratedOrderCode} + + Crud + update + TransactionJournal + + + Publish PE to Issue Actual Vouchers + + Equals + MemberId + {!TransactionJournal.MemberId} + + + Equals + TotalVouchersToIssue + {!LineItemTotalVoucherToIssue} + + + Equals + OrderId + {!GeneratedOrderCode} + + + Equals + ProgramId + {!TransactionJournal.LoyaltyProgramId} + + RunFlow + Loyalty_Process_Publish_Event_Process_Rewards + + + Run Debit Points and Journals for Voucher Issuance + + Equals + ActivityDate + {!TransactionJournal.ActivityDate} + + + Equals + Order_ID__c + {!GeneratedOrderCode} + + + Equals + Voucher_Count__c + {!LineItemTotalVoucherToIssue} + + + Equals + MemberId + {!TransactionJournal.MemberId} + + + Equals + JournalDate + {!TransactionJournal.JournalDate} + + + Equals + TransactionAmount + {!PointsToDebitForVoucherIssue} + + + Equals + Channel_Name__c + {!TransactionJournal.Channel} + + + Equals + Channel + {!TransactionJournal.Channel} + + RunProgramProcess + Rewards Redemption Process + + + (1 OR 2 ) AND 3 AND 4 + + Equals + 1 + TransactionJournal.TransactionAmount + NULL + Formula + + + Equals + 2 + TransactionJournal.TransactionAmount + 0 + Literal + + + Equals + 3 + TransactionJournal.Order_Item_ID__c + '' + Formula + + + Equals + 4 + TransactionJournal.Voucher_Definition_Name__c + '' + Formula + + IF Transaction Amount = zero/null AND OrderItemID = Blank AND VoucherDef = Blank + Condition + + + IF Transaction Amount NOT NULL + ConditionGroup + + + 1 + + GreaterThan + 1 + TransactionJournal.TransactionAmount + 0 + Literal + + And Amount > 0 + Condition + + + 1 + + LessThan + 1 + TransactionJournal.TransactionAmount + 0 + Literal + + And Amount < 0 + Condition + + + 1 + + GreaterThanOrEquals + 1 + PositiveBalanceCurrency + VoucherTargetPoints + Parameter + + IF Positive Balance Currency is POSITIVE and Greater than Voucher Target Points + Condition + + false + Bulk Voucher Upload + Set Up Step + 2024-03-01 + Active + + IF Transaction Amount = zero/null AND OrderItemID = Blank AND VoucherDef = Blank + 1 + + + IF Transaction Amount NOT NULL + 3 + + + And Amount > 0 + IF Transaction Amount NOT NULL + 1 + + + And Amount < 0 + IF Transaction Amount NOT NULL + 2 + + + IF Positive Balance Currency is POSITIVE and Greater than Voucher Target Points + 5 + + + Retrieve Points for Adjustment from Ledgers + IF Transaction Amount = zero/null AND OrderItemID = Blank AND VoucherDef = Blank + 1 + + + GET Runtime VoucherDefinition Values + 2 + + + Credit Adjustment Points + And Amount > 0 + 1 + + + Debit Adjustment Points + And Amount < 0 + 1 + + + GET Positive Balance Currency [After Adjustment ] + 4 + + + Assign Values for Vouchers Issuance + IF Positive Balance Currency is POSITIVE and Greater than Voucher Target Points + 1 + + + Generate OrderID Code + IF Positive Balance Currency is POSITIVE and Greater than Voucher Target Points + 2 + + + Update Journal OrderId field + IF Positive Balance Currency is POSITIVE and Greater than Voucher Target Points + 3 + + + Publish PE to Issue Actual Vouchers + IF Positive Balance Currency is POSITIVE and Greater than Voucher Target Points + 4 + + + Run Debit Points and Journals for Voucher Issuance + IF Positive Balance Currency is POSITIVE and Greater than Voucher Target Points + 5 + + + + + Process to award points to new member enrolled into the program + RealTime + Member Enrollment + Accrual + Member Enrollment Process + TransactionJournal + Active + + Numeric + 0 + false + false + false + PointsBalance + Variable + + + + GET Positive Points Balance + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsBalance + PointsBalance + + GetMemberPointBalance + + + SYNC Rewards Tracking EA with PointsBalance + + Equals + EngagementAttributeName + Rewards Tracking + Literal + + + Equals + UpdateType + OverwriteCurrentValue + + + Equals + Value + {!PointsBalance} + + UpdateCurrentValueForMemberAttribute + + false + New Enrolment Bonus + Enrolment Reward Points Sync + 2024-03-01 + Active + + GET Positive Points Balance + 1 + + + SYNC Rewards Tracking EA with PointsBalance + 2 + + + + + Credit Bonus Points + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsToCredit + 50 + + CreditPoints + + + Update Member - Status Points + + Equals + _action + update + + + Equals + _entity + LoyaltyProgramMember + + + Equals + Id + {!TransactionJournal.MemberId} + + + Equals + Status_Points__c + 50 + + + Equals + LastActivityDate + DATEVALUE({!TransactionJournal.ActivityDate} ) + + Crud + update + LoyaltyProgramMember + + false + Member Enrolment Promotion + New Enrolment Bonus + 2024-03-01 + Active + + Credit Bonus Points + 1 + + + Update Member - Status Points + 2 + + + + + This process is to Update the currency balance and Engagement Attribute points + BatchAndRealTime + Member Migration + Accrual + Member Migration Process + TransactionJournal + Active + + Numeric + 0 + false + false + false + TransactionAmount + Variable + + + Numeric + 0 + false + false + false + UpdatedPositiveBalanceTrackingEA + Variable + + + Numeric + 0 + false + false + false + UpdatedRewardsTrackingEA + Variable + + + Boolean + false + false + false + isNegative + Variable + + + + Set Parameters For Transaction Amount + + Equals + TransactionAmount + 1 + CEILING({!TransactionJournal.TransactionAmount} ) + + AssignParameterValues + + + Update Member Attribute Negative Tracking Value + + Equals + UpdateType + OverwriteCurrentValue + + + Equals + Value + {!TransactionAmount} *(-1) + + + Equals + EngagementAttributeName + Negative Balance Tracking + Literal + + UpdateCurrentValueForMemberAttribute + + + Update Currency Negative Balance + + Equals + ProgramCurrencyName + Negative Balance + Literal + + + Equals + PointsToCredit + {!TransactionAmount} *(-1) + + CreditPoints + + + Update Member Status Point Balance + + Equals + _action + update + + + Equals + Id + {!TransactionJournal.MemberId} + + + Equals + Status_Points__c + {!TransactionAmount} + + + Equals + _entity + LoyaltyProgramMember + + Crud + update + LoyaltyProgramMember + + + Update Currency Positive Balance + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsToCredit + {!TransactionAmount} + + CreditPoints + + + Update Positive Balance in Member Status Point Balance Field + + Equals + _action + update + + + Equals + _entity + LoyaltyProgramMember + + + Equals + Status_Points__c + {!TransactionAmount} + + + Equals + Id + {!TransactionJournal.MemberId} + + Crud + update + LoyaltyProgramMember + + + Update Member Attribute Positive Tracking Value + + Equals + EngagementAttributeName + Positive Balance Tracking + Literal + + + Equals + UpdateType + OverwriteCurrentValue + + + Equals + Value + {!TransactionAmount} + + + Equals + MemberAttributeCurrentValue + UpdatedPositiveBalanceTrackingEA + + UpdateCurrentValueForMemberAttribute + + + Update Member Attribute Rewards Tracking Value + + Equals + EngagementAttributeName + Rewards Tracking + Literal + + + Equals + Value + {!TransactionAmount} + + + Equals + MemberAttributeCurrentValue + UpdatedRewardsTrackingEA + + + Equals + UpdateType + OverwriteCurrentValue + + UpdateCurrentValueForMemberAttribute + + + 1 + + LessThan + 1 + TransactionAmount + 0 + Literal + + Check if Transaction Amount is Negative + Condition + + + 1 + + GreaterThanOrEquals + 1 + TransactionAmount + 0 + Literal + + Check if Transaction Amount is Positive + Condition + + false + Getting Transaction Amount From Journal + 1970-01-01 + Active + + Check if Transaction Amount is Negative + 2 + + + Check if Transaction Amount is Positive + 3 + + + Set Parameters For Transaction Amount + 1 + + + Update Member Attribute Negative Tracking Value + Check if Transaction Amount is Negative + 1 + + + Update Currency Negative Balance + Check if Transaction Amount is Negative + 2 + + + Update Member Status Point Balance + Check if Transaction Amount is Negative + 3 + + + Update Currency Positive Balance + Check if Transaction Amount is Positive + 1 + + + Update Positive Balance in Member Status Point Balance Field + Check if Transaction Amount is Positive + 2 + + + Update Member Attribute Positive Tracking Value + Check if Transaction Amount is Positive + 3 + + + Update Member Attribute Rewards Tracking Value + Check if Transaction Amount is Positive + 4 + + + + + + Numeric + 0 + false + false + false + CurrentEngagementAttributeValue + Variable + + + Numeric + 0 + false + false + false + debitpointsForNegativeBalance + Variable + + + Numeric + 0 + false + false + false + debitpointsForPositiveBalance + Variable + + + Date + false + false + false + setLastActivityDatetoNull + Variable + + + Numeric + 0 + Negative Point Balance Variable + false + false + false + storeMemberNegativePointBalanceValue + Variable + + + Numeric + 0 + Store Positive Point Balance + false + false + false + storeMemberPositivePointBalanceValue + Variable + + + Date + false + false + false + todaysDate + Variable + + Process to reset and expire points + Batch + Points Expiration + Points Expiration Process + TransactionJournal + Active + + + Get Member's Negative Point Balance + + Equals + ProgramCurrencyName + Negative Balance + Literal + + + Equals + PointsBalance + storeMemberNegativePointBalanceValue + + GetMemberPointBalance + + + Get Member's Positive Point Balance + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsBalance + storeMemberPositivePointBalanceValue + + GetMemberPointBalance + + + Debit Point From Negative Balance + + Equals + ProgramCurrencyName + Negative Balance + Literal + + + Equals + PointsToDebit + {!storeMemberNegativePointBalanceValue} + + DebitPoints + + + Reset Positive Point Balance to 0 + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsToDebit + {!storeMemberPositivePointBalanceValue} + + DebitPoints + + + 1 + + NotEquals + 1 + storeMemberNegativePointBalanceValue + 0 + Literal + + Check if Member Negative Point Balance Value < 0 + Condition + + + 1 + + NotEquals + 1 + storeMemberPositivePointBalanceValue + 0 + Literal + + Check if Positive Point Balance Value > 0 + Condition + + false + Reset Member Engagement Attributes + Reset Member Currencies + 2024-04-04 + Active + + Check if Member Negative Point Balance Value < 0 + 3 + + + Check if Positive Point Balance Value > 0 + 4 + + + Get Member's Negative Point Balance + 1 + + + Get Member's Positive Point Balance + 2 + + + Debit Point From Negative Balance + Check if Member Negative Point Balance Value < 0 + 1 + + + Reset Positive Point Balance to 0 + Check if Positive Point Balance Value > 0 + 1 + + + + + Current Engagement Attribute Value + + Equals + EngagementAttributeName + Rewards Tracking + Literal + + + Equals + MemberAttributeCurrentValue + CurrentEngagementAttributeValue + + GetMemberAttributesValues + + + Set Engagement Value = 0 + + Equals + EngagementAttributeName + Rewards Tracking + Literal + + + Equals + UpdateType + OverwriteCurrentValue + + + Equals + Value + 0 + + UpdateCurrentValueForMemberAttribute + + + 1 + + GreaterThan + 1 + CurrentEngagementAttributeValue + 0 + Literal + + Check if Current Engagement Value > 0 + Condition + + false + Reset Member Engagement Attributes + 2024-04-04 + Active + + Check if Current Engagement Value > 0 + 2 + + + Current Engagement Attribute Value + 1 + + + Set Engagement Value = 0 + Check if Current Engagement Value > 0 + 1 + + + + + Update Member Status Point Balance to 0 + + Equals + _action + update + + + Equals + _entity + LoyaltyProgramMember + + + Equals + Id + {!TransactionJournal.Member.Id} + + + Equals + Status_Points__c + 0 + + + Equals + LastActivityDate + TODAY() + + Crud + update + LoyaltyProgramMember + + + 1 + + GreaterThan + 1 + TransactionJournal.Member.Status_Points__c + 0 + Literal + + Check if Member Status Point Value > 0 + Condition + + false + Reset Member Currencies + Reset Member Status Points + 2024-04-04 + Active + + Check if Member Status Point Value > 0 + 1 + + + Update Member Status Point Balance to 0 + Check if Member Status Point Value > 0 + 1 + + + + + + DateTime + false + false + false + ActivityDate + Formula + {!TransactionJournal.ActivityDate} + + + Numeric + 0 + false + false + false + BasePointMultiplier + Variable + + + Numeric + 0 + Used to store transaction amount as 'Ceiling' ie rounded number. + false + false + false + BasePointsEarned + Variable + + + Numeric + 0 + Used in a Accrual-Purchase process in IF block, check if previous balance is negative, which impacts on whether new voucher will be generated + false + false + false + BeginningNegativeBalance + Variable + + + Numeric + 0 + false + false + false + BeginningTotalRewards + Variable + + + Numeric + 0 + false + false + false + BrandMultiplier + Variable + + + Date + false + false + false + CurrentDate + Formula + TODAY() + + + Numeric + 0 + Used to limit only a single lineitem will Dummy voucher node in API response + false + false + false + CurrentLineItemCount + Variable + + + Numeric + 0 + false + false + false + CurrentMemberStatusPoints + Variable + + + Numeric + 0 + false + false + false + CurrentReturnTracking + Variable + + + Numeric + 0 + false + false + false + CurrentReversedVoucherTotalFV + Variable + + + Numeric + 0 + false + false + false + CurrentTotalRewards + Variable + + + Text + Output from Decision Table which is used to multiply the Transaction Amount for 'effective points', eg. $1 x 1 = 1 point, $1 x 2 = 2 points + false + false + false + DT_Multiplier + Variable + + + Sobject + to define what format will be the output from Decision Table + false + false + false + Accrual_Purchase_Multiplier__c + DT_Outcome + Variable + + + Numeric + 0 + Count of how many times to execute direct voucher issue + false + false + false + DirectIssueTotalCount + Variable + + + Numeric + 0 + Counter to track how many voucher issued directly in Process + false + false + false + DirectIssueVoucherCounter + Variable + + + Numeric + 0 + this multiplier is 'delta' on top of Base. Eg. if Base =1 and promotion is to give 2x , then this multiplier is 2 - 1 = 1 (being the delta) + false + false + false + DoublePointsPromo_DELTA_Multiplier + Constant + 1 + + + Numeric + 0 + store beginning balance in its own dedicated EA, (snapshot store), because the running balance will change due to new Order + false + false + false + EA_BeginningRewardsTracking + Variable + + + Boolean + this is flag to indicate whether we need to generate new voucher due to new order + false + false + false + EA_IsRewardsTargetMet + Variable + + + Numeric + 0 + to track on LINEITEM level, how much if redeem voucher amount, used to ensure Transaction Amount for Purchase journal with VoucherCode will be 'zero-ized' ie. Transaction amount - this LineItemVoucherFaceValue amount + false + false + false + EA_LineItemRedeemVoucherFaceValue + Variable + + + Numeric + 0 + stores cumulative value of pro rata apportionment of reduction in negative balance by lineitems' points + false + false + false + EA_NegativeBalanceApportionment + Variable + + + Numeric + 0 + this always equal to Negative Currency value, used for LINEITEM level context in our calculation of change to Currency (debit or credit) + false + false + false + EA_NegativeBalanceTracking + Variable + + + Numeric + 0 + tracks at lineitem level the rewards earned within this single Order + false + false + false + EA_PerLIneItemOrderRewardsTracking + Variable + + + Numeric + 0 + for a new Order with Redemption of Vouchers, which could be multiple, we use this to total them up (total facevalue) + false + false + false + EA_PerOrderRedeemVoucherFaceValue + Variable + + + Numeric + 0 + to track on a Per Order basis, ie single Order API call, how many 'new' points for that Order + false + false + false + EA_PerOrderRewardsTracking + Variable + + + Numeric + 0 + this always equal to Positive Currency value, used for LINEITEM level context in our calculation of change to Currency (debit or credit) + false + false + false + EA_PositiveBalanceTracking + Variable + + + Numeric + 0 + this keeps whatever is 'left' after using points that qualify for new voucher (ie meeting target voucher points), eg. 250 points - 200 points is used for new voucher, what is left = 50 + false + false + false + EA_RemainderRewardsBalance + Variable + + + Numeric + 0 + this is 'piggy bank' level tracking, how many points collected so far and whether the 'target' is reached yet + false + false + false + EA_RewardsTracking + Variable + + + Text + store vouchercode generated by Flow for use in Direct Issue Voucher + false + false + false + GeneratedVoucherCode + Variable + + + Boolean + false + false + false + IsNonMerchanisedItem + Variable + + + Boolean + false + false + false + IsTargetMet + Variable + + + Numeric + 0 + this is the Single lineitem Dummy voucher face amount + false + false + false + LineItemTotalVoucherFaceValue + Variable + + + Numeric + 0 + this is the count of total number of vouchers to issue , used in generate voucher flow to create voucher records + false + false + false + LineItemTotalVoucherToIssue + Variable + + + Numeric + 0 + this stores the value retrieved from the Negative Currency 'get' + false + false + false + MC_NegativeCurrencyPointsBalance + Variable + + + Numeric + 0 + this stores the value retrieved from the Positive Currency 'get' + false + false + false + MC_PositiveCurrencyPointsBalance + Variable + + + Text + {!TransactionJournal.MemberId} + false + false + false + MemberID + Formula + {!TransactionJournal.MemberId} + + + Numeric + 2 + false + false + false + Multiplier + Variable + + + Numeric + 0 + actual vouchers to issue after deducting 3 vouchers issued directly via Process + false + false + false + NettTotalVoucherToIssue + Variable + + + Text + {!TransactionJournal.Order_ID__c} + false + false + false + OrderID + Formula + {!TransactionJournal.Order_ID__c} + + + Numeric + 0 + stores currency balance whenever needed for calculations + false + false + false + PointsBalance + Variable + + + Numeric + 0 + false + false + false + PointsDebited + Variable + + + Numeric + 0 + CEILING({!DT_Outcome.Multiplier__c} *{!BasePointsEarned}) + false + false + false + PointsForPurchase + Formula + CEILING({!DT_Outcome.Multiplier__c} *{!BasePointsEarned}) + + + Numeric + 0 + stores the total of points that qualifies for voucher (can be multiple of voucherTargetPoints), in single Order + false + false + false + PointsToDebitForVoucherIssue + Variable + + + Text + {!TransactionJournal.LoyaltyProgramId} + false + false + false + ProgramID + Formula + {!TransactionJournal.LoyaltyProgramId} + + + Numeric + 0 + NOT USED except v5 + false + false + false + PurchasePointsAdjustedRemainder + Variable + + + Numeric + 0 + parameter used in calculations, stores the total per Order points 'earned' by a new Order + false + false + false + TotalOrderPurchasePoints + Variable + + + DateTime + false + false + false + TransactionJournalActivityDate + Variable + + + Text + false + false + false + VoucherCodeIssued + Variable + + + Numeric + 2 + stores the value of a single voucher , eg $10 per voucher + false + false + false + VoucherFaceValue + Variable + + + Numeric + 0 + the 'threshold' of how many points needed to meet 'goal' ie. qualify for voucher issuance + false + false + false + VoucherTargetPoints + Variable + + Latest Version 6 - Only of Accrual process + RealTime + Purchase + Accrual + Purchase Accrual Process + TransactionJournal + Active + + + Run Redeem Voucher Process + + Equals + ActivityDate + {!TransactionJournal.ActivityDate} + + + Equals + Channel_Name__c + {!TransactionJournal.Channel} + + + Equals + Order_ID__c + {!TransactionJournal.Order_ID__c} + + + Equals + Brand + {!TransactionJournal.Brand} + + + Equals + Order_Item_ID__c + {!TransactionJournal.VoucherCode} + + + Equals + MemberId + {!MemberID} + + + Equals + VoucherCode + {!TransactionJournal.VoucherCode} + + + Equals + LoyaltyProgramId + {!ProgramID} + + + Equals + JournalDate + {!TransactionJournal.JournalDate} + + + Equals + TransactionAmount + {!TransactionJournal.TransactionAmount} + + RunProgramProcess + Redeem Voucher Process + + + GET LineItem Redeem Voucher FaceValue + + Equals + MemberAttributeCurrentValue + EA_LineItemRedeemVoucherFaceValue + + + Equals + EngagementAttributeName + Redeem Voucher FaceValueTracking + Literal + + GetMemberAttributesValues + + + Set the EA_LineItemRedeemVoucherFaceValue to Zero. + + Equals + EA_LineItemRedeemVoucherFaceValue + 1 + 0 + + AssignParameterValues + + + Credit PositiveBalance Currency ADD Promo EA_PerLineItemOrderRewardsTracking [if voucherCode lineitem; less voucher FV] + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsToCredit + {!EA_PerLIneItemOrderRewardsTracking} + {!PointsForPurchase} - {!EA_LineItemRedeemVoucherFaceValue} + + CreditPoints + + + GET RedeemVoucher FaceValue Consolidated + + Equals + EngagementAttributeName + RedeemVoucherFaceValue Consolidated + Literal + + + Equals + MemberAttributeCurrentValue + EA_PerOrderRedeemVoucherFaceValue + + GetMemberAttributesValues + + + GET Positive Balance Currency after Credits + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsBalance + MC_PositiveCurrencyPointsBalance + + GetMemberPointBalance + + + A. Initialize LineItemCount + + Equals + Value + 0 + + + Equals + MemberAttributeCurrentValue + CurrentLineItemCount + + + Equals + EngagementAttributeName + LineItemCount + Literal + + + Equals + UpdateType + OverwriteCurrentValue + + UpdateCurrentValueForMemberAttribute + + + A. Increment LineItemCount + + Equals + Value + 1 + + + Equals + MemberAttributeCurrentValue + CurrentLineItemCount + + + Equals + EngagementAttributeName + LineItemCount + Literal + + + Equals + UpdateType + IncreaseCurrentValue + + UpdateCurrentValueForMemberAttribute + + + Assign Values to IssueVoucher Params (whole order basis) & Initialize DirectIssueVoucherCounter = 0 + + Equals + PointsToDebitForVoucherIssue + 1 + FLOOR({!MC_PositiveCurrencyPointsBalance} / {!VoucherTargetPoints} )*{!VoucherTargetPoints} + + + Equals + LineItemTotalVoucherToIssue + 2 + FLOOR({!MC_PositiveCurrencyPointsBalance} / {!VoucherTargetPoints} ) + + + Equals + LineItemTotalVoucherFaceValue + 3 + FLOOR({!MC_PositiveCurrencyPointsBalance} / {!VoucherTargetPoints} )*{!VoucherFaceValue} + + + Equals + DirectIssueVoucherCounter + 4 + 0 + + AssignParameterValues + + + 1. Set DirectIssueTotalCount Value to 3 + + Equals + DirectIssueTotalCount + 1 + 3 + + AssignParameterValues + + + 2. Set DirectIssueTotalCount Value = [LineItemTotalVoucherToIssue] + + Equals + DirectIssueTotalCount + 1 + {!LineItemTotalVoucherToIssue} + + AssignParameterValues + + + 1. Generate Voucher Code + + Equals + VoucherDefinitionName + "Reward" + + + Equals + VoucherCount + 1 + + + Equals + VoucherCodesToGenerate + 1 + + + Equals + VoucherCodes + GeneratedVoucherCode + + RunFlow + Loyalty_Process_Generate_Voucher_Codes + + + IssueVoucher Count 1 + + Equals + VoucherCode + {!GeneratedVoucherCode} + + + Equals + VoucherDefinitionName + Reward + Literal + + IssueVoucher + + + Set Counter = 1 + + Equals + DirectIssueVoucherCounter + 1 + 1 + + AssignParameterValues + + + Publish Direct Issue Voucher Event 1 + + Equals + OrderIdIssuedChannel + {!TransactionJournal.Channel} + + + Equals + VoucherCode + {!GeneratedVoucherCode} + + + Equals + MemberId + {!MemberID} + + + Equals + OrderIdIssued + {!OrderID} + + RunFlow + Loyalty_Process_Publish_Direct_Issue_Voucher_Event + + + Generate Voucher Code 2 + + Equals + VoucherCount + 1 + + + Equals + VoucherCodes + GeneratedVoucherCode + + + Equals + VoucherDefinitionName + "Reward" + + + Equals + VoucherCodesToGenerate + 1 + + RunFlow + Loyalty_Process_Generate_Voucher_Codes + + + IssueVoucher Count 2 + + Equals + VoucherDefinitionName + Reward + Literal + + + Equals + VoucherCode + {!GeneratedVoucherCode} + + IssueVoucher + + + Set Counter = 2 + + Equals + DirectIssueVoucherCounter + 1 + 2 + + AssignParameterValues + + + Publish Direct Issue Voucher Event 2 + + Equals + OrderIdIssued + {!OrderID} + + + Equals + MemberId + {!MemberID} + + + Equals + OrderIdIssuedChannel + {!TransactionJournal.Channel} + + + Equals + VoucherCode + {!GeneratedVoucherCode} + + RunFlow + Loyalty_Process_Publish_Direct_Issue_Voucher_Event + + + Generate Voucher Code 3 + + Equals + VoucherCodesToGenerate + 1 + + + Equals + VoucherCount + 1 + + + Equals + VoucherCodes + GeneratedVoucherCode + + + Equals + VoucherDefinitionName + "Reward" + + RunFlow + Loyalty_Process_Generate_Voucher_Codes + + + IssueVoucher Count 3 + + Equals + VoucherDefinitionName + Reward + Literal + + + Equals + VoucherCode + {!GeneratedVoucherCode} + + IssueVoucher + + + Set Counter = 3 + + Equals + DirectIssueVoucherCounter + 1 + 3 + + AssignParameterValues + + + Publish Direct Issue Voucher Event 3 + + Equals + OrderIdIssuedChannel + {!TransactionJournal.Channel} + + + Equals + VoucherCode + {!GeneratedVoucherCode} + + + Equals + MemberId + {!MemberID} + + + Equals + OrderIdIssued + {!OrderID} + + RunFlow + Loyalty_Process_Publish_Direct_Issue_Voucher_Event + + + Set Nett TotalVouchersToIssue + + Equals + NettTotalVoucherToIssue + 1 + {!LineItemTotalVoucherToIssue} - {!DirectIssueVoucherCounter} + + AssignParameterValues + + + Publish PE to Issue Actual Vouchers + + Equals + OrderId + {!OrderID} + + + Equals + TotalVouchersToIssue + {!NettTotalVoucherToIssue} + + + Equals + ProgramId + {!ProgramID} + + + Equals + Channel + {!TransactionJournal.Channel} + + + Equals + MemberId + {!MemberID} + + RunFlow + Loyalty_Process_Publish_Event_Process_Rewards + + + Run Debit Points + + Equals + ActivityDate + {!TransactionJournal.ActivityDate} + + + Equals + PartnerId + {!TransactionJournal.PartnerId} + + + Equals + Channel_Name__c + {!TransactionJournal.Channel} + + + Equals + Order_ID__c + {!OrderID} + + + Equals + Voucher_Count__c + {!LineItemTotalVoucherToIssue} + + + Equals + Channel + {!TransactionJournal.Channel} + + + Equals + Order_Item_ID__c + {!TransactionJournal.Order_Item_ID__c} + + + Equals + MemberId + {!MemberID} + + + Equals + JournalDate + {!TransactionJournal.JournalDate} + + + Equals + TransactionAmount + {!PointsToDebitForVoucherIssue} + + RunProgramProcess + Rewards Redemption Process + + + RESET PerOrder Rewards Tracking + + Equals + UpdateType + OverwriteCurrentValue + + + Equals + Value + 0 + + + Equals + EngagementAttributeName + PerOrder Rewards Tracking + Literal + + UpdateCurrentValueForMemberAttribute + + + RESET PerLineItemOrderRewards Tracking + + Equals + EngagementAttributeName + PerLineItemOrderRewards Tracking + Literal + + + Equals + Value + 0 + + + Equals + UpdateType + OverwriteCurrentValue + + UpdateCurrentValueForMemberAttribute + + + RESET RedeemVoucherFaceValueTracking + + Equals + UpdateType + OverwriteCurrentValue + + + Equals + EngagementAttributeName + Redeem Voucher FaceValueTracking + Literal + + + Equals + Value + 0 + + UpdateCurrentValueForMemberAttribute + + + RESET RedeemVoucherFaceValue Consolidated Tracking + + Equals + Value + 0 + + + Equals + EngagementAttributeName + RedeemVoucherFaceValue Consolidated + Literal + + + Equals + UpdateType + OverwriteCurrentValue + + UpdateCurrentValueForMemberAttribute + + + RESET LineItemCount to Zero + + Equals + EngagementAttributeName + LineItemCount + Literal + + + Equals + UpdateType + OverwriteCurrentValue + + + Equals + Value + 0 + + UpdateCurrentValueForMemberAttribute + + + 1 + + NotEquals + 1 + TransactionJournal.VoucherCode + NULL + Formula + + PreCheck : IF VoucherCode EXIST + Condition + + + 1 + + Equals + 1 + PointsForPurchase + 0 + Literal + + If the Brand value is null + Condition + + + 1 AND 2 + + GreaterThanOrEquals + 1 + MC_PositiveCurrencyPointsBalance + VoucherTargetPoints + Parameter + + + NotEquals + 2 + DT_Outcome.Multiplier__c + 0 + Literal + + A. IF Updated Currency Balance >= VoucherPointsTarget + Condition + + + 1 + + Equals + 1 + CurrentLineItemCount + 1 + Literal + + A.IF LineItemCount = 1 + Condition + + + Set Counter Value based on [LineItemTotalVouchersToIssue] based on scenario + ConditionGroup + + + 1 + + GreaterThan + 1 + LineItemTotalVoucherToIssue + 3 + Literal + + LineItemTotalVouchersToIssue > 3 + Condition + + + 1 + + LessThanOrEquals + 1 + LineItemTotalVoucherToIssue + 3 + Literal + + LineItemTotalVouchersToIssue <= 3 + Condition + + + 1 + + LessThan + 1 + DirectIssueVoucherCounter + DirectIssueTotalCount + Parameter + + Count 1 : If [DirectIssueVoucherCounter] < DirectIssueTotalCount + Condition + + + 1 + + LessThan + 1 + DirectIssueVoucherCounter + DirectIssueTotalCount + Parameter + + Count 2 : If [DirectIssueVoucherCounter] < DirectIssueTotalCount + Condition + + + 1 + + LessThan + 1 + DirectIssueVoucherCounter + DirectIssueTotalCount + Parameter + + Count 3 : If [DirectIssueVoucherCounter] < DirectIssueTotalCount + Condition + + + 1 + + GreaterThan + 1 + LineItemTotalVoucherToIssue + 3 + Literal + + If TotalVouchers to Issuse > 3 + Condition + + Base rule execution step + false + Double Points Promo + Base Rule Execution + 2024-03-01 + Active + + PreCheck : IF VoucherCode EXIST + 1 + + + If the Brand value is null + PreCheck : IF VoucherCode EXIST + 3 + + + A. IF Updated Currency Balance >= VoucherPointsTarget + 5 + + + A.IF LineItemCount = 1 + A. IF Updated Currency Balance >= VoucherPointsTarget + 3 + + + Set Counter Value based on [LineItemTotalVouchersToIssue] based on scenario + A.IF LineItemCount = 1 + 2 + + + LineItemTotalVouchersToIssue > 3 + Set Counter Value based on [LineItemTotalVouchersToIssue] based on scenario + 1 + + + LineItemTotalVouchersToIssue <= 3 + Set Counter Value based on [LineItemTotalVouchersToIssue] based on scenario + 2 + + + Count 1 : If [DirectIssueVoucherCounter] < DirectIssueTotalCount + A.IF LineItemCount = 1 + 3 + + + Count 2 : If [DirectIssueVoucherCounter] < DirectIssueTotalCount + A.IF LineItemCount = 1 + 4 + + + Count 3 : If [DirectIssueVoucherCounter] < DirectIssueTotalCount + A.IF LineItemCount = 1 + 5 + + + If TotalVouchers to Issuse > 3 + A.IF LineItemCount = 1 + 6 + + + Run Redeem Voucher Process + PreCheck : IF VoucherCode EXIST + 1 + + + GET LineItem Redeem Voucher FaceValue + PreCheck : IF VoucherCode EXIST + 2 + + + Set the EA_LineItemRedeemVoucherFaceValue to Zero. + If the Brand value is null + 1 + + + Credit PositiveBalance Currency ADD Promo EA_PerLineItemOrderRewardsTracking [if voucherCode lineitem; less voucher FV] + 2 + + + GET RedeemVoucher FaceValue Consolidated + 3 + + + GET Positive Balance Currency after Credits + 4 + + + A. Initialize LineItemCount + A. IF Updated Currency Balance >= VoucherPointsTarget + 1 + + + A. Increment LineItemCount + A. IF Updated Currency Balance >= VoucherPointsTarget + 2 + + + Assign Values to IssueVoucher Params (whole order basis) & Initialize DirectIssueVoucherCounter = 0 + A.IF LineItemCount = 1 + 1 + + + 1. Set DirectIssueTotalCount Value to 3 + LineItemTotalVouchersToIssue > 3 + 1 + + + 2. Set DirectIssueTotalCount Value = [LineItemTotalVoucherToIssue] + LineItemTotalVouchersToIssue <= 3 + 1 + + + 1. Generate Voucher Code + Count 1 : If [DirectIssueVoucherCounter] < DirectIssueTotalCount + 1 + + + IssueVoucher Count 1 + Count 1 : If [DirectIssueVoucherCounter] < DirectIssueTotalCount + 2 + + + Set Counter = 1 + Count 1 : If [DirectIssueVoucherCounter] < DirectIssueTotalCount + 3 + + + Publish Direct Issue Voucher Event 1 + Count 1 : If [DirectIssueVoucherCounter] < DirectIssueTotalCount + 4 + + + Generate Voucher Code 2 + Count 2 : If [DirectIssueVoucherCounter] < DirectIssueTotalCount + 1 + + + IssueVoucher Count 2 + Count 2 : If [DirectIssueVoucherCounter] < DirectIssueTotalCount + 2 + + + Set Counter = 2 + Count 2 : If [DirectIssueVoucherCounter] < DirectIssueTotalCount + 3 + + + Publish Direct Issue Voucher Event 2 + Count 2 : If [DirectIssueVoucherCounter] < DirectIssueTotalCount + 4 + + + Generate Voucher Code 3 + Count 3 : If [DirectIssueVoucherCounter] < DirectIssueTotalCount + 1 + + + IssueVoucher Count 3 + Count 3 : If [DirectIssueVoucherCounter] < DirectIssueTotalCount + 2 + + + Set Counter = 3 + Count 3 : If [DirectIssueVoucherCounter] < DirectIssueTotalCount + 3 + + + Publish Direct Issue Voucher Event 3 + Count 3 : If [DirectIssueVoucherCounter] < DirectIssueTotalCount + 4 + + + Set Nett TotalVouchersToIssue + If TotalVouchers to Issuse > 3 + 1 + + + Publish PE to Issue Actual Vouchers + If TotalVouchers to Issuse > 3 + 2 + + + Run Debit Points + A.IF LineItemCount = 1 + 7 + + + RESET PerOrder Rewards Tracking + 6 + + + RESET PerLineItemOrderRewards Tracking + 7 + + + RESET RedeemVoucherFaceValueTracking + 8 + + + RESET RedeemVoucherFaceValue Consolidated Tracking + 9 + + + RESET LineItemCount to Zero + 10 + + + + + PositiveBalanceCurrency - Points Balance + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsBalance + MC_PositiveCurrencyPointsBalance + + GetMemberPointBalance + + + Update Member - Status Points + + Equals + _action + update + + + Equals + _entity + LoyaltyProgramMember + + + Equals + Id + {!MemberID} + + + Equals + Status_Points__c + {!MC_PositiveCurrencyPointsBalance} - {!MC_NegativeCurrencyPointsBalance} + + + Equals + LastActivityDate + DATEVALUE({!TransactionJournal.ActivityDate} ) + + Crud + update + LoyaltyProgramMember + + false + Base Rule Execution + Base Rule Final + 2024-03-01 + Active + + PositiveBalanceCurrency - Points Balance + 1 + + + Update Member - Status Points + 2 + + + + + GET Positive Balance Currency + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsBalance + MC_PositiveCurrencyPointsBalance + + GetMemberPointBalance + + + Assign Beginning Negative Balance + + Equals + BeginningNegativeBalance + 1 + ABS({!MC_PositiveCurrencyPointsBalance} ) + + AssignParameterValues + + + Set Parameter + + Equals + BasePointsEarned + 1 + CEILING({!TransactionJournal.TransactionAmount} ) + + + Equals + VoucherFaceValue + 2 + VALUE("10.0") + + + Equals + VoucherTargetPoints + 3 + 200 + + AssignParameterValues + + + Set BasePoints to Zero + + Equals + BasePointsEarned + 1 + 0 + + AssignParameterValues + + + Run Flow - Get Runtime Configuration + + Equals + VoucherRewardTargetPoints + VoucherTargetPoints + + RunFlow + Loyalty_Process_Get_Runtime_Configuration + + + Get Decision Table Outcome + + Equals + Brand__c + {!TransactionJournal.Brand} + + + Equals + DecisionTableOutcomeType + DT_Multiplier + + + Equals + DecisionTableSingleOutcome + DT_Outcome + + GetOutputsFromDecisionTable + Accrual_Purchase_Multiplier + + + 1 + + LessThan + 1 + MC_PositiveCurrencyPointsBalance + 0 + Literal + + IF Positive Balance is NEGATIVE + Condition + + + 1 OR 2 + + Equals + 1 + TransactionJournal.Is_Non_Merchanised_Item__c + true + Literal + + + Equals + 2 + TransactionJournal.Product_Category_Name__c + Non-Merchandised + Literal + + If Non Merchandise + Condition + + false + Base Rule Setup + 2024-03-01 + Active + + IF Positive Balance is NEGATIVE + 2 + + + If Non Merchandise + 4 + + + GET Positive Balance Currency + 1 + + + Assign Beginning Negative Balance + IF Positive Balance is NEGATIVE + 1 + + + Set Parameter + 3 + + + Set BasePoints to Zero + If Non Merchandise + 1 + + + Run Flow - Get Runtime Configuration + 5 + + + Get Decision Table Outcome + 6 + + + + + Overwrite for PerLineItemOrder Rewards Tracking + + Equals + EngagementAttributeName + PerLineItemOrderRewards Tracking + Literal + + + Equals + MemberAttributeCurrentValue + EA_PerLIneItemOrderRewardsTracking + + + Equals + UpdateType + OverwriteCurrentValue + + + Equals + Value + {!DoublePointsPromo_DELTA_Multiplier} * {!PointsForPurchase} + + UpdateCurrentValueForMemberAttribute + + + 1 AND (2 OR 3 OR 4 OR 5 OR 6 OR 7 OR 8 OR 9 OR 10 OR (11 AND 12)) + + Equals + 1 + TransactionJournal.VoucherCode + NULL + Formula + + + Contains + 2 + TransactionJournal.Product.ProductCode + EJNZ:528012 + Literal + + + Contains + 3 + TransactionJournal.Product.ProductCode + EJNZ:515241 + Literal + + + Contains + 4 + TransactionJournal.Product.ProductCode + EJNZ:417062 + Literal + + + Contains + 5 + TransactionJournal.Product.ProductCode + EJNZ:286043 + Literal + + + Contains + 6 + TransactionJournal.Product.ProductCode + EJNZ:488147 + Literal + + + Contains + 7 + TransactionJournal.Product.ProductCode + EJ:353507 + Literal + + + Contains + 8 + TransactionJournal.Product.ProductCode + EJ:752195 + Literal + + + Contains + 9 + TransactionJournal.Product.ProductCode + JJ:688155 + Literal + + + Contains + 10 + TransactionJournal.Product.ProductCode + JJ:667473 + Literal + + + Equals + 11 + TransactionJournal.Brand + Portmans + Literal + + + Equals + 12 + TransactionJournal.Product.Country__c + Australia + Literal + + IF product code qualifies + Condition + + false + Base Rule Setup + Double Points Promo + Double Points Promo + 2024-03-01 + Active + + IF product code qualifies + 1 + + + Overwrite for PerLineItemOrder Rewards Tracking + IF product code qualifies + 1 + + + + + + Text + false + false + false + Channel + Variable + + + Numeric + 0 + false + false + false + CurrentRedeemVoucherFaceValue + Variable + + + Numeric + 0 + false + false + false + EA_RedeemVoucherFaceValueTracking + Variable + + + Text + false + false + false + VoucherId + Variable + + Create a Loyalty Process for Type = Accrual and Sub Type = Voucher. + RealTime + Voucher + Accrual + Redeem Voucher Process + TransactionJournal + Active + + + Redeem Voucher Using Voucher Code + + Equals + VoucherCode + {!TransactionJournal.VoucherCode} + + + Equals + AmountToRedeem + {!TransactionJournal.TransactionAmount} + + RedeemVoucher + + + Increment Face Value of voucher redemption + + Equals + Value + {!TransactionJournal.TransactionAmount} + + + Equals + MemberAttributeCurrentValue + CurrentRedeemVoucherFaceValue + + + Equals + EngagementAttributeName + Redeem Voucher FaceValueTracking + Literal + + + Equals + UpdateType + IncreaseCurrentValue + + UpdateCurrentValueForMemberAttribute + + + GET RedeemVoucherFaceValueTracking EA + + Equals + EngagementAttributeName + Redeem Voucher FaceValueTracking + Literal + + + Equals + MemberAttributeCurrentValue + EA_RedeemVoucherFaceValueTracking + + GetMemberAttributesValues + + + Overwrite RedeemVoucherFaceValue Consolidated EA + + Equals + EngagementAttributeName + RedeemVoucherFaceValue Consolidated + Literal + + + Equals + UpdateType + OverwriteCurrentValue + + + Equals + Value + {!EA_RedeemVoucherFaceValueTracking} + + UpdateCurrentValueForMemberAttribute + + + Debug RVFV_RedeemProcess + + Equals + Message1 + "'Debug RVFV_RedeemProcess'" + + + Equals + Message4 + 'OrderId : ' + {!TransactionJournal.Order_ID__c} + + + Equals + Message3 + 'ChannelName : ' + {!TransactionJournal.Channel_Name__c} + + + Equals + Message2 + 'Debug RVFV_RedeemProcess RVFV : ' + TEXT({!CurrentRedeemVoucherFaceValue} ) + + RunFlow + Loyalty_Process_Debug_Process + + + Publish Voucher Redeem Event + + Equals + VoucherCode + {!TransactionJournal.VoucherCode} + + + Equals + Channel + {!TransactionJournal.Channel_Name__c} + + + Equals + OrderNo + {!TransactionJournal.Order_ID__c} + + RunFlow + Publish_Voucher_Redeem_Event + + Redeem Voucher and associate Order via PE + false + RedeemVoucherAndUpdateOrderUsingPE + 2024-04-01 + Active + + Redeem Voucher Using Voucher Code + 1 + + + Increment Face Value of voucher redemption + 2 + + + GET RedeemVoucherFaceValueTracking EA + 3 + + + Overwrite RedeemVoucherFaceValue Consolidated EA + 4 + + + Debug RVFV_RedeemProcess + 5 + + + Publish Voucher Redeem Event + 6 + + + + + RealTime + Admin Adjustment + Accrual + Reset Return Tracking Attribute Value + TransactionJournal + Active + + + Reset Return Tracking Value to 0 + + Equals + EngagementAttributeName + Return Tracking + Literal + + + Equals + UpdateType + OverwriteCurrentValue + + + Equals + Value + 0 + + UpdateCurrentValueForMemberAttribute + + false + Reset Return Tracking Attribute + 2024-04-01 + Active + + Reset Return Tracking Value to 0 + 1 + + + + + + Numeric + 0 + This Stores Transaction Amount from Journal + false + false + false + BasePointsToReverse + Variable + + + Numeric + 0 + false + false + false + BeginningTotalRewards + Variable + + + Text + false + false + false + ConcatenatedOriginalOrderAmount + Variable + + + Text + false + false + false + ConcatenatedPointsCreditedOutput + Variable + + + Text + false + false + false + ConcatenatedTotalReturnAmount + Variable + + + Text + false + false + false + ConcatenatedreturnChannelValue + Variable + + + Numeric + 0 + keeps track of Count for each journal entry passed in via a Single API + false + false + false + CurrentLineItemCount + Variable + + + Numeric + 0 + Variable to store output of current Return Tracking Engagement Attribute Value after Increment + false + false + false + CurrentReturnTrackingValue + Variable + + + Numeric + 0 + false + false + false + CurrentTotalRewards + Variable + + + Text + Variable to store Output from Decision Table + false + false + false + DT_Multiplier + Variable + + + Sobject + Variable to store Brand Multiplier from Decision Table + false + false + false + Accrual_Purchase_Multiplier__c + DT_Outcome + Variable + + + Numeric + 0 + Variable to store Current Negative Balance Tracking Engagement Attribute + false + false + false + EA_NegativeBalanceTracking + Variable + + + Numeric + 0 + Variable to store Current Positive Balance Tracking Engagement Attribute + false + false + false + EA_PositiveBalanceTracking + Variable + + + Numeric + 0 + Variable to store current value from Return Tracking Engagement Attribute + false + false + false + EA_ReturnTrackingOutPut + Variable + + + Numeric + 0 + false + false + false + EachLineItemPointsToReverse + Variable + + + Numeric + 0 + Variable to store Positive Tracking Engagement attribute current value before debit + false + false + false + GetPositiveEAValueBeforeDebit + Variable + + + Boolean + Parameter to store Boolean value (True) if return is full return + false + false + false + IsFullReturn + Variable + + + Boolean + false + false + false + JournalsNotFound + Variable + + + Numeric + 0 + false + false + false + LineItemPointsCredited + Variable + + + Numeric + 0 + Variable to store Current Negative Balance Currency + false + false + false + MC_NegativeCurrencyPointsBalance + Variable + + + Numeric + 0 + Variable to store Current Positive Balance Currency + false + false + false + MC_PositiveCurrencyPointsBalance + Variable + + + Numeric + 0 + Variable to store Current Negative Balance points of members + false + false + false + MemberNegativeBalance + Variable + + + Numeric + 0 + Variable to store Current Positive Balance points of members + false + false + false + MemberPositiveBalance + Variable + + + Text + false + false + false + OrderIdItemChannelKey + Variable + + + Text + false + false + false + OriginalOrderAmountKey + Variable + + + Text + Variable to store Original Order Id from Transaction Journal + false + false + false + OriginalOrderId + Variable + + + Text + false + false + false + OriginalOrderIdOuput + Variable + + + Numeric + 0 + Points to Reverse + false + false + false + PointsToReverse + Formula + {!TotalAmountCreditedForPurchase} + + + Numeric + 0 + Variable to store output from flow - Get points for which voucher was Issued + false + false + false + Points_for_which_Voucher_was_Issued + Variable + + + Numeric + 0 + Output variable to store total amount credited for purchase on Original Order + false + false + false + TotalAmountCreditedForPurchase + Variable + + + Numeric + 0 + Output variable to store total original amount of original order from Transaction journal. (Output from flow) + false + false + false + TotalAmountOfOriginalOrderIdFromTJ + Variable + + + Numeric + 0 + Parameter to store total amount of original order ; in order to compare whether full or partial refund + false + false + false + TotalOriginalOrderAmt + Variable + + + Numeric + 0 + store found redeemed vouchers and their sum total value, being reversed to be 'issued' again + false + false + false + TotalVoucherFaceValueReversed + Variable + + + Numeric + 0 + Output variable to store total voucher count for Order Issued Voucher + false + false + false + VoucherCountForOrderIdIssued + Variable + + + Boolean + Variable to store output from flow - to get voucher details + false + false + false + VoucherIssuedOnOrder + Variable + + + Boolean + Variable to store output from flow - to get voucher details + false + false + false + VoucherRedeemedForOrder + Variable + + + Numeric + 0 + false + false + false + perOrderReturnAmount + Variable + + + Text + false + false + false + returnPerOrderChannel + Variable + + + Text + false + false + false + returnPerOrderChannelKey + Variable + + + Text + false + false + false + returnPerOrderTotalAmountKey + Variable + + RealTime + Reversal + Accrual + Reversal for Return Process + TransactionJournal + Active + + + PositiveBalanceCurrency - Points Balance + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsBalance + MC_PositiveCurrencyPointsBalance + + GetMemberPointBalance + + + NegativeBalanceCurrency - Points Balance + + Equals + ProgramCurrencyName + Negative Balance + Literal + + + Equals + PointsBalance + MC_NegativeCurrencyPointsBalance + + GetMemberPointBalance + + + Sync Rewards Tracking with Positive Currency Balance + + Equals + EngagementAttributeName + Rewards Tracking + Literal + + + Equals + UpdateType + OverwriteCurrentValue + + + Equals + Value + {!MC_PositiveCurrencyPointsBalance} + + UpdateCurrentValueForMemberAttribute + + + Sync Rewards Tracking to ZERO + + Equals + EngagementAttributeName + Rewards Tracking + Literal + + + Equals + UpdateType + OverwriteCurrentValue + + + Equals + Value + 0 + + UpdateCurrentValueForMemberAttribute + + + Update Member - Status Points + + Equals + _action + update + + + Equals + _entity + LoyaltyProgramMember + + + Equals + Id + {!TransactionJournal.Member.Id} + + + Equals + Status_Points__c + {!MC_PositiveCurrencyPointsBalance} - {!MC_NegativeCurrencyPointsBalance} + + + Equals + LastActivityDate + DATEVALUE({!TransactionJournal.ActivityDate} ) + + Crud + update + LoyaltyProgramMember + + + 1 + + GreaterThanOrEquals + 1 + MC_PositiveCurrencyPointsBalance + 0 + Literal + + If Positive Balance + Condition + + + 1 + + LessThan + 1 + MC_PositiveCurrencyPointsBalance + 0 + Literal + + If Negative Balance + Condition + + false + Reversal Rule + Final Rule + 2024-04-01 + Active + + If Positive Balance + 3 + + + If Negative Balance + 4 + + + PositiveBalanceCurrency - Points Balance + 1 + + + NegativeBalanceCurrency - Points Balance + 2 + + + Sync Rewards Tracking with Positive Currency Balance + If Positive Balance + 1 + + + Sync Rewards Tracking to ZERO + If Negative Balance + 1 + + + Update Member - Status Points + 5 + + + + + Set Parameters + + Equals + BasePointsToReverse + 1 + CEILING({!TransactionJournal.TransactionAmount} ) + + + Equals + OriginalOrderId + 2 + {!TransactionJournal.Original_Order_ID__c} + + + Equals + CurrentLineItemCount + 3 + 0 + + + Equals + IsFullReturn + 4 + false + + + Equals + TotalVoucherFaceValueReversed + 5 + 0 + + AssignParameterValues + + + Get Decision Table Outcome + + Equals + Brand__c + {!TransactionJournal.Brand} + + + Equals + DecisionTableOutcomeType + DT_Multiplier + + + Equals + DecisionTableSingleOutcome + DT_Outcome + + GetOutputsFromDecisionTable + Accrual_Purchase_Multiplier + + + Assign ZERO to BasePoints + + Equals + BasePointsToReverse + 1 + 0 + + AssignParameterValues + + + Return Tracking EA Current Value + + Equals + EngagementAttributeName + Return Tracking + Literal + + + Equals + MemberAttributeCurrentValue + EA_ReturnTrackingOutPut + + GetMemberAttributesValues + + + Get Original Points Credited for Purchase perLineItem and perOrder + + Equals + OriginalOrderItemId + {!TransactionJournal.Original_Order_Item_ID__c} + + + Equals + OriginalOrderId + {!TransactionJournal.Original_Order_ID__c} + + + Equals + ReturnChannel + {!TransactionJournal.Channel} + + + Equals + OriginalChannel + {!TransactionJournal.Original_Channel__c} + + + Equals + TransactionAmount + {!BasePointsToReverse} + + + Equals + ReturnOrderId + {!TransactionJournal.Order_ID__c} + + + Equals + ReturnOrderItemId + {!TransactionJournal.Order_Item_ID__c} + + + Equals + memberId + {!TransactionJournal.MemberId} + + + Equals + ConcatenatedOutput + ConcatenatedPointsCreditedOutput + + + Equals + ConcatenatedReturnChannelValue + ConcatenatedreturnChannelValue + + + Equals + ConcatenatedTotalReturnAmount + ConcatenatedTotalReturnAmount + + RunFlow + Return_total_points_credited + + + Assign PerOrderReturnChannelKey + + Equals + returnPerOrderChannelKey + 1 + {!TransactionJournal.Order_ID__c} & '@' + + AssignParameterValues + + + Extract PerOrderReturnChannel value + + Equals + returnPerOrderChannel + 1 + MID( {!ConcatenatedreturnChannelValue} , + FIND({!returnPerOrderChannelKey} , {!ConcatenatedreturnChannelValue} ) + LEN({!returnPerOrderChannelKey} ), + FIND('@' , RIGHT({!ConcatenatedreturnChannelValue} , LEN({!ConcatenatedreturnChannelValue} ) - (FIND({!returnPerOrderChannelKey} ,{!ConcatenatedreturnChannelValue} ) +LEN({!returnPerOrderChannelKey} )) )) + ) + + AssignParameterValues + + + Debug Channel Value + + Equals + Message2 + 'returnPerOrderChannel = ' & {!returnPerOrderChannel} + + + Equals + Message1 + "'debug Channel Value'" + + RunFlow + Loyalty_Process_Debug_Process + + + Assign OrderIdItemChannelKey@ + + Equals + OrderIdItemChannelKey + 1 + {!TransactionJournal.Order_ID__c} & + {!TransactionJournal.Order_Item_ID__c} & + {!returnPerOrderChannel} & + {!TransactionJournal.Original_Order_ID__c} &{!TransactionJournal.Original_Order_Item_ID__c} & {!TransactionJournal.Original_Channel__c} & '@' + + + Equals + returnPerOrderTotalAmountKey + 2 + {!TransactionJournal.Order_ID__c} &{!TransactionJournal.Order_Item_ID__c} &{!returnPerOrderChannel} & '@' + + AssignParameterValues + + + Assign EachLineItemPointsToReverse = BasePointsToReverse + + Equals + EachLineItemPointsToReverse + 1 + {!BasePointsToReverse} + + AssignParameterValues + + + Assign per LineItem Points to Reverse & perOrder ReturnTotalAmount + + Equals + EachLineItemPointsToReverse + 1 + VALUE( + MID( + {!ConcatenatedPointsCreditedOutput} , + FIND({!OrderIdItemChannelKey} , {!ConcatenatedPointsCreditedOutput} ) + LEN({!OrderIdItemChannelKey} ), + FIND('@' , RIGHT({!ConcatenatedPointsCreditedOutput} , + LEN({!ConcatenatedPointsCreditedOutput}) - (FIND({!OrderIdItemChannelKey},{!ConcatenatedPointsCreditedOutput} ) + +LEN({!OrderIdItemChannelKey})) )) + ) + ) + + + Equals + perOrderReturnAmount + 2 + VALUE( + MID( + {!ConcatenatedTotalReturnAmount} , + FIND({!returnPerOrderTotalAmountKey} , {!ConcatenatedTotalReturnAmount} ) + LEN({!returnPerOrderTotalAmountKey} ), + FIND('@' , RIGHT({!ConcatenatedTotalReturnAmount} , + LEN({!ConcatenatedTotalReturnAmount} ) - (FIND({!returnPerOrderTotalAmountKey} ,{!ConcatenatedTotalReturnAmount} ) + +LEN({!returnPerOrderTotalAmountKey} )) )) + ) + ) + + AssignParameterValues + + + Debug Original Points Credited + + Equals + Message5 + 'Starting position of digit =' & + TEXT(FIND({!returnPerOrderChannelKey} , {!ConcatenatedreturnChannelValue} ) + LEN({!returnPerOrderChannelKey} )) & ' ; ' & + 'Ending position digit =' & + TEXT( FIND('@' , RIGHT({!ConcatenatedreturnChannelValue} , + LEN({!ConcatenatedreturnChannelValue} ) - (FIND({!returnPerOrderChannelKey} ,{!ConcatenatedreturnChannelValue} ) + +LEN({!returnPerOrderChannelKey} )) )) + ) + + + Equals + Message4 + 'returnPerOrderChannel = ' & {!returnPerOrderChannel} + + + Equals + Message3 + 'EachLineItemPointsToReverse = ' & TEXT( {!EachLineItemPointsToReverse} ) + + + Equals + Message2 + 'TransactionJournal.Order_ID__c = ' & {!TransactionJournal.Order_ID__c} + + + Equals + Message1 + "'Debug Original Points Credited'" + + RunFlow + Loyalty_Process_Debug_Process + + + Positive Balance Tracking EA Current Value + + Equals + MemberAttributeCurrentValue + EA_PositiveBalanceTracking + + + Equals + EngagementAttributeName + Positive Balance Tracking + Literal + + GetMemberAttributesValues + + + Positive Balance + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsBalance + MemberPositiveBalance + + GetMemberPointBalance + + + Get Original Transaction Amount of Order + + Equals + OriginalOrderID + {!OriginalOrderId} + + + Equals + memberId + {!TransactionJournal.Member.Id} + + + Equals + ConcatenatedOriOrderAmount + ConcatenatedOriginalOrderAmount + + + Equals + OriginalTotalAmount + TotalAmountOfOriginalOrderIdFromTJ + + RunFlow + Handle_Return_Process_TJ + + + Assign OriginalOrderId Key + + Equals + OriginalOrderAmountKey + 1 + {!TransactionJournal.Original_Order_ID__c} &'@' + + AssignParameterValues + + + Extract Total Orginal Order Amount per OriginalOrderId + + Equals + TotalOriginalOrderAmt + 1 + VALUE( MID( {!ConcatenatedOriginalOrderAmount} , FIND({!OriginalOrderAmountKey} , {!ConcatenatedOriginalOrderAmount} ) + LEN({!OriginalOrderAmountKey} ), FIND('@' , RIGHT({!ConcatenatedOriginalOrderAmount} , LEN({!ConcatenatedOriginalOrderAmount} ) - (FIND({!OriginalOrderAmountKey} ,{!ConcatenatedOriginalOrderAmount} ) +LEN({!OriginalOrderAmountKey} )) )) ) ) + + AssignParameterValues + + + Debug Return Amt vs Original + + Equals + Message5 + 'TotalOriginalOrderAmt = ' & TEXT({!TotalOriginalOrderAmt} ) & ' ; ' & + 'TotalAmountOfOriginalOrderIdFromTJ = ' & TEXT({!TotalAmountOfOriginalOrderIdFromTJ} ) + + + Equals + Message4 + 'OriginalOrderId = ' & {!OriginalOrderId} & + 'TotalOriginalOrderAmt = ' & TEXT({!TotalOriginalOrderAmt} ) + + + Equals + Message3 + 'perOrderReturnAmount = ' & TEXT({!perOrderReturnAmount} ) + + + Equals + Message2 + 'EachLineItemPointsToReverse = ' & TEXT( {!EachLineItemPointsToReverse} ) + + + Equals + Message1 + "'Debug Return Amt vs Original'" + + RunFlow + Loyalty_Process_Debug_Process + + + Full Return Flag = TRUE + + Equals + IsFullReturn + 1 + True + + AssignParameterValues + + + Debug if Full Return + + Equals + Message3 + 'CurrentLineItemCount = ' & TEXT({!CurrentLineItemCount} ) + + + Equals + Message2 + 'IsFullReturn = ' & IF( {!IsFullReturn} , 'True', 'False') + + + Equals + Message1 + "'Debug if full Return'" + + RunFlow + Loyalty_Process_Debug_Process + + + Get Reversed Redeemed Vouchers Total FaceValue Amount + + Equals + MemberId + {!TransactionJournal.MemberId} + + + Equals + OriginalOrderId + {!TransactionJournal.Original_Order_ID__c} + + + Equals + OriginalChannel + {!TransactionJournal.Original_Channel__c} + + + Equals + TotalVoucherFaceValueReversed + TotalVoucherFaceValueReversed + + RunFlow + Transaction_Journal_Reversal_VoucherReversal_Value + + + Debug input for Voucher Reverse + + Equals + Message5 + 'TotalVoucherFaceValueReversed = ' & TEXT( {!TotalVoucherFaceValueReversed} ) + + + Equals + Message4 + 'TransactionJournal.Original_Order_ID__c} = ' & {!TransactionJournal.Original_Order_ID__c} + + + Equals + Message3 + 'TransactionJournal.MemberId = ' & {!TransactionJournal.MemberId} + + + Equals + Message2 + 'TransactionJournal.Original_Channel__c = ' & {!TransactionJournal.Original_Channel__c} + + + Equals + Message1 + "'Debug Voucher Reverse'" + + RunFlow + Loyalty_Process_Debug_Process + + + Publish Event to Reverse Redeemed Voucher Status to Issued + + Equals + MemberId + {!TransactionJournal.MemberId} + + + Equals + OriginalOrderId + {!TransactionJournal.Original_Order_ID__c} + + + Equals + OriginalChannel + {!TransactionJournal.Original_Channel__c} + + RunFlow + Publish_Event_for_Reversal_of_Redeemed_Vouchers + + + Overwrite ReversedVoucherFaceValue EA + + Equals + EngagementAttributeName + ReversedVoucherTotalFaceValue + Literal + + + Equals + UpdateType + OverwriteCurrentValue + + + Equals + Value + {!TotalVoucherFaceValueReversed} + + UpdateCurrentValueForMemberAttribute + + + Debit Points + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsToDebit + {!EachLineItemPointsToReverse} + + DebitPoints + + + RESET LineItemCount + + Equals + Value + 0 + + + Equals + EngagementAttributeName + LineItemCount + Literal + + + Equals + UpdateType + OverwriteCurrentValue + + UpdateCurrentValueForMemberAttribute + + + RESET Return Tracking EA + + Equals + Value + 0 + + + Equals + UpdateType + OverwriteCurrentValue + + + Equals + EngagementAttributeName + Return Tracking + Literal + + UpdateCurrentValueForMemberAttribute + + + 1 OR 2 OR 3 + + Equals + 1 + TransactionJournal.Is_Non_Merchanised_Item__c + true + Literal + + + Equals + 2 + TransactionJournal.Product_Category_Name__c + Non-Merchandised + Literal + + + Equals + 3 + DT_Outcome.Multiplier__c + 0 + Literal + + If Non Merchandise + Condition + + + 1 + + NotEquals + 1 + TransactionJournal.Original_Order_ID__c + NULL + Formula + + IF Original OrderID Exist + Condition + + + 1 + + Equals + 1 + TransactionJournal.Original_Order_ID__c + NULL + Formula + + IF Original OrderID is NULL + Condition + + + 1 AND 2 AND 3 + + Equals + 1 + perOrderReturnAmount + TotalOriginalOrderAmt + Parameter + + + NotEquals + 2 + BasePointsToReverse + 0 + Literal + + + NotEquals + 3 + TransactionJournal.Original_Order_ID__c + NULL + Formula + + Check if perOrderReturnAmount = Original Amount [Full Return] + Condition + + + 1 + + Equals + 1 + IsFullReturn + true + Literal + + If Returned Order's Original Order is full return (scenario 7A) AND LineItemCount = 1 + Condition + + false + Reversal Rule + 2024-04-01 + Active + + If Non Merchandise + 3 + + + IF Original OrderID Exist + 5 + + + IF Original OrderID is NULL + 6 + + + Check if perOrderReturnAmount = Original Amount [Full Return] + 15 + + + If Returned Order's Original Order is full return (scenario 7A) AND LineItemCount = 1 + Check if perOrderReturnAmount = Original Amount [Full Return] + 3 + + + Set Parameters + 1 + + + Get Decision Table Outcome + 2 + + + Assign ZERO to BasePoints + If Non Merchandise + 1 + + + Return Tracking EA Current Value + 4 + + + Get Original Points Credited for Purchase perLineItem and perOrder + IF Original OrderID Exist + 1 + + + Assign PerOrderReturnChannelKey + IF Original OrderID Exist + 2 + + + Extract PerOrderReturnChannel value + IF Original OrderID Exist + 3 + + + Debug Channel Value + IF Original OrderID Exist + 4 + + + Assign OrderIdItemChannelKey@ + IF Original OrderID Exist + 5 + + + Assign EachLineItemPointsToReverse = BasePointsToReverse + IF Original OrderID is NULL + 1 + + + Assign per LineItem Points to Reverse & perOrder ReturnTotalAmount + 7 + + + Debug Original Points Credited + 8 + + + Positive Balance Tracking EA Current Value + 9 + + + Positive Balance + 10 + + + Get Original Transaction Amount of Order + 11 + + + Assign OriginalOrderId Key + 12 + + + Extract Total Orginal Order Amount per OriginalOrderId + 13 + + + Debug Return Amt vs Original + 14 + + + Full Return Flag = TRUE + Check if perOrderReturnAmount = Original Amount [Full Return] + 1 + + + Debug if Full Return + Check if perOrderReturnAmount = Original Amount [Full Return] + 2 + + + Get Reversed Redeemed Vouchers Total FaceValue Amount + If Returned Order's Original Order is full return (scenario 7A) AND LineItemCount = 1 + 1 + + + Debug input for Voucher Reverse + If Returned Order's Original Order is full return (scenario 7A) AND LineItemCount = 1 + 2 + + + Publish Event to Reverse Redeemed Voucher Status to Issued + If Returned Order's Original Order is full return (scenario 7A) AND LineItemCount = 1 + 3 + + + Overwrite ReversedVoucherFaceValue EA + If Returned Order's Original Order is full return (scenario 7A) AND LineItemCount = 1 + 4 + + + Debit Points + 16 + + + RESET LineItemCount + 17 + + + RESET Return Tracking EA + 18 + + + + + + Text + Channel associated with the Order ID. Must use channel name formula field + false + false + false + Channel + Formula + {!TransactionJournal.Channel_Name__c} + + + Numeric + 0 + Store total number of accrual items being processed so far + false + false + false + EA_AccrualItemTracking + Variable + + + Numeric + 0 + Store to current Positive Balance engagement attribute + false + false + false + EA_PositiveBalanceTracking + Variable + + + Numeric + 0 + NOT BEING USED + false + false + false + EA_RedemptionTracking + Variable + + + Text + Set Event Type = Redemption + false + false + false + EventType + Constant + Redemption + + + Text + This is the loyalty member ID to process the journal + false + false + false + MemberID + Formula + {!TransactionJournal.MemberId} + + + Text + Order ID associated with this Redemption + false + false + false + OrderID + Formula + {!TransactionJournal.Order_ID__c} + + + Text + Order Item ID associated with this Redemption + false + false + false + OrderItemID + Formula + {!TransactionJournal.Order_Item_ID__c} + + + Numeric + 0 + Store the current Positive Balance Currency + false + false + false + PositiveBalanceCurrency + Variable + + + Text + Loyalty Program ID + false + false + false + ProgramID + Formula + {!TransactionJournal.LoyaltyProgram.Id} + + + Text + Loyalty Program Name + false + false + false + ProgramName + Variable + + + Numeric + 0 + This is the per-calculated points to deduct. from the currency. Since each accrual journal will attempt to deduct points we need to divide the points to deduct based on # of accrual items being processed. + false + false + false + TotalPointsToDebit + Formula + {!TotalVouchersToIssue} * {!VoucherTargetPoints} + + + Numeric + 0 + Total # of vouchers to issue + false + false + false + TotalVouchersToIssue + Formula + {!TransactionJournal.Voucher_Count__c} + + + Numeric + 2 + Face value of reward vouchers being issued + false + false + false + VoucherFaceValue + Variable + + + Text + Voucher Definition Name being used to issue reward vouchers + false + false + false + VoucherRewardDefinition + Variable + + + Numeric + 0 + Target points to issue reward vouchers + false + false + false + VoucherTargetPoints + Variable + + Handle rewards redemption by debiting points + RealTime + Reward + Redemption + Rewards Redemption Process + TransactionJournal + Active + + + Run Flow - Get Runtime Configuration + + Equals + VoucherRewardDefinitionName + VoucherRewardDefinition + + + Equals + VoucherRewardTargetPoints + VoucherTargetPoints + + + Equals + LoyaltyProgramName + ProgramName + + + Equals + VoucherRewardFaceValue + VoucherFaceValue + + RunFlow + Loyalty_Process_Get_Runtime_Configuration + + + Get PositiveBalanceCurrency + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsBalance + PositiveBalanceCurrency + + GetMemberPointBalance + + + Run Flow - Debug + + Equals + Message5 + TEXT( {!EA_AccrualItemTracking} ) + + + Equals + Message3 + TEXT( {!EA_PositiveBalanceTracking} ) + + + Equals + Message2 + TEXT( {!PositiveBalanceCurrency} ) + + + Equals + Message1 + TEXT( {!TotalPointsToDebit} ) + + + Equals + Message4 + {!TransactionJournal.Channel_Name__c} + + RunFlow + Loyalty_Process_Debug_Process + + + Debit Positive Balance Currency + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsToDebit + {!TotalPointsToDebit} + + DebitPoints + + + GET Positive Balance Currency + + Equals + ProgramCurrencyName + Positive Balance + Literal + + + Equals + PointsBalance + PositiveBalanceCurrency + + GetMemberPointBalance + + + Update Member - Status Points + + Equals + _action + update + + + Equals + _entity + LoyaltyProgramMember + + + Equals + Id + {!MemberID} + + + Equals + Status_Points__c + {!PositiveBalanceCurrency} + + Crud + update + LoyaltyProgramMember + + + Overwrite RewardTracking EA with Current Positive Balance + + Equals + EngagementAttributeName + Rewards Tracking + Literal + + + Equals + UpdateType + OverwriteCurrentValue + + + Equals + Value + {!PositiveBalanceCurrency} + + UpdateCurrentValueForMemberAttribute + + + Run Flow - Handle Redemption Journal + + Equals + EventType + {!EventType} + + + Equals + Channel + {!TransactionJournal.Channel_Name__c} + + + Equals + TotalPointsToDebit + {!TotalPointsToDebit} + + + Equals + TotalRewards + {!TotalPointsToDebit} + + + Equals + OrderID + {!OrderID} + + + Equals + ProgramID + {!ProgramID} + + + Equals + OrderItemID + {!OrderItemID} + + + Equals + MemberID + {!MemberID} + + RunFlow + Loyalty_Process_Process_Rewards + + + 1 + + GreaterThan + 1 + PositiveBalanceCurrency + 0 + Literal + + If Positive Balance > 0 + Condition + + false + Base Rule + 2024-04-01 + Active + + If Positive Balance > 0 + 7 + + + Run Flow - Get Runtime Configuration + 1 + + + Get PositiveBalanceCurrency + 2 + + + Run Flow - Debug + 3 + + + Debit Positive Balance Currency + 4 + + + GET Positive Balance Currency + 5 + + + Update Member - Status Points + 6 + + + Overwrite RewardTracking EA with Current Positive Balance + If Positive Balance > 0 + 1 + + + Run Flow - Handle Redemption Journal + 8 + + + + + + Text + Update Apple Wallet Link field on Loyalty Member record + false + true + false + AppleWalletLink + Variable + + + Date + Update Birthdate on Person Account record + false + true + false + Birthdate + Variable + + + Text + Identifier of the contact that's associated with the loyalty program member. + false + true + true + ContactId + Variable + + + Text + Update country field on Loyalty Member record + false + true + false + Country + Variable + + + Text + Email address of the member. + false + true + false + Email + Variable + + + Text + Name of the channel used for enrolling in the loyalty program. + false + true + false + EnrollmentChannel + Variable + + + Date + Date on which the member enrolled in the loyalty program. + false + true + false + EnrollmentDate + Variable + + + Text + A given name of the member. + false + true + false + FirstName + Variable + + + Text + Update Google Wallet Link field on Loyalty Member record + false + true + false + GoogleWalletLink + Variable + + + Text + Surname or the family name of the member. + false + true + false + LastName + Variable + + + Text + The identifier of the loyalty program member. + false + true + true + MemberId + Variable + + + Text + Update Member Status field to re-enrol Loyalty Member + false + true + false + MemberStatus + Variable + + + Text + The membership number of the loyalty program member. + false + true + false + MembershipNumber + Variable + + + Text + Mobile phone number of the member. + false + true + false + MobilePhone + Variable + + + Boolean + Update Staff field on Loyalty Member record + false + true + false + Staff + Variable + + + Text + Update State field on Loyalty Member record + false + true + false + State + Variable + + + Text + false + false + true + TransactionJournalId + Variable + + + + Update Member Record + + Equals + _entity + LoyaltyProgramMember + + + Equals + Id + {!MemberId} + + + Equals + State__c + {!State} + + + Equals + EnrollmentChannel + {!EnrollmentChannel} + + + Equals + Google_Wallet_Link__c + {!GoogleWalletLink} + + + Equals + MembershipNumber + {!MembershipNumber} + + + Equals + EnrollmentDate + {!EnrollmentDate} + + + Equals + Country__c + {!Country} + + + Equals + Staff__c + {!Staff} + + + Equals + Apple_Wallet_Link__c + {!AppleWalletLink} + + + Equals + _outputId + MemberId + + + Equals + _action + update + + Crud + update + LoyaltyProgramMember + + + Update Member's Contact Record + + Equals + _action + update + + + Equals + _entity + Contact + + + Equals + Id + {!ContactId} + + + Equals + Email + {!Email} + + + Equals + MobilePhone + {!MobilePhone} + + + Equals + LastName + {!LastName} + + + Equals + FirstName + {!FirstName} + + + Equals + _outputId + ContactId + + + Equals + Birthdate + {!Birthdate} + + Crud + update + Contact + + Modifies the member record and the associated contact record. + false + Update Member Details + 2024-03-14 + Active + + Update Member Record + 1 + + + Update Member's Contact Record + 2 + + + Updates member details such as first name, last name, email ID, and mobile phone number. + RealTime + UpdateMemberDetails + UpdateMemberDetails + Active + + \ No newline at end of file diff --git a/src/handlers/disassemble.rs b/src/handlers/disassemble.rs index bb30a72..98a0b1d 100644 --- a/src/handlers/disassemble.rs +++ b/src/handlers/disassemble.rs @@ -1,7 +1,12 @@ //! Disassemble XML file handler. use crate::builders::build_disassembled_files_unified; -use crate::types::BuildDisassembledFilesOptions; +use crate::multi_level::{ + capture_xmlns_from_root, path_segment_from_file_pattern, save_multi_level_config, + strip_root_and_build_xml, +}; +use crate::parsers::parse_xml; +use crate::types::{BuildDisassembledFilesOptions, MultiLevelRule}; use ignore::gitignore::GitignoreBuilder; use std::path::Path; use tokio::fs; @@ -56,6 +61,7 @@ impl DisassembleXmlFileHandler { post_purge: bool, ignore_path: &str, format: &str, + multi_level_rule: Option<&MultiLevelRule>, ) -> Result<(), Box> { let strategy = strategy.unwrap_or("unique-id"); let strategy = if ["unique-id", "grouped-by-tag"].contains(&strategy) { @@ -85,6 +91,7 @@ impl DisassembleXmlFileHandler { pre_purge, post_purge, format, + multi_level_rule, ) .await?; } else if meta.is_dir() { @@ -95,6 +102,7 @@ impl DisassembleXmlFileHandler { pre_purge, post_purge, format, + multi_level_rule, ) .await?; } @@ -112,6 +120,7 @@ impl DisassembleXmlFileHandler { pre_purge: bool, post_purge: bool, format: &str, + multi_level_rule: Option<&MultiLevelRule>, ) -> Result<(), Box> { let resolved = Path::new(file_path) .canonicalize() @@ -140,10 +149,12 @@ impl DisassembleXmlFileHandler { pre_purge, post_purge, format, + multi_level_rule, ) .await } + #[allow(clippy::too_many_arguments)] async fn handle_directory( &self, dir_path: &str, @@ -152,6 +163,7 @@ impl DisassembleXmlFileHandler { pre_purge: bool, post_purge: bool, format: &str, + multi_level_rule: Option<&MultiLevelRule>, ) -> Result<(), Box> { let mut entries = fs::read_dir(dir_path).await?; let cwd = std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf()); @@ -177,6 +189,7 @@ impl DisassembleXmlFileHandler { pre_purge, post_purge, format, + multi_level_rule, ) .await?; } @@ -195,6 +208,7 @@ impl DisassembleXmlFileHandler { pre_purge: bool, post_purge: bool, format: &str, + multi_level_rule: Option<&MultiLevelRule>, ) -> Result<(), Box> { log::debug!("Parsing file to disassemble: {}", file_path); @@ -218,7 +232,137 @@ impl DisassembleXmlFileHandler { unique_id_elements, strategy, }) - .await + .await?; + + if let Some(rule) = multi_level_rule { + self.recursively_disassemble_multi_level(&output_path, rule, format) + .await?; + } + + Ok(()) + } + + /// Recursively walk the disassembly output; for XML files matching the rule's file_pattern, + /// strip the root and re-disassemble with the rule's unique_id_elements. + async fn recursively_disassemble_multi_level( + &self, + dir_path: &Path, + rule: &MultiLevelRule, + format: &str, + ) -> Result<(), Box> { + let mut config = crate::multi_level::load_multi_level_config(dir_path) + .await + .unwrap_or_default(); + + let mut stack = vec![dir_path.to_path_buf()]; + while let Some(current) = stack.pop() { + let mut entries = Vec::new(); + let mut read_dir = fs::read_dir(¤t).await?; + while let Some(entry) = read_dir.next_entry().await? { + entries.push(entry); + } + + for entry in entries { + let path = entry.path(); + let path_str = path.to_string_lossy().to_string(); + + if path.is_dir() { + stack.push(path); + } else if path.is_file() { + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + let path_str_check = path.to_string_lossy(); + if !name.ends_with(".xml") + || (!name.contains(&rule.file_pattern) + && !path_str_check.contains(&rule.file_pattern)) + { + continue; + } + + let parsed = match parse_xml(&path_str).await { + Some(p) => p, + None => continue, + }; + let has_element_to_strip = parsed + .as_object() + .and_then(|o| { + let root_key = o.keys().find(|k| *k != "?xml")?; + let root_val = o.get(root_key)?.as_object()?; + Some( + root_key == &rule.root_to_strip + || root_val.contains_key(&rule.root_to_strip), + ) + }) + .unwrap_or(false); + if !has_element_to_strip { + continue; + } + + let wrap_xmlns = capture_xmlns_from_root(&parsed).unwrap_or_default(); + + let stripped_xml = match strip_root_and_build_xml(&parsed, &rule.root_to_strip) + { + Some(xml) => xml, + None => continue, + }; + + fs::write(&path, stripped_xml).await?; + + let file_stem = path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("output"); + let output_dir_name = file_stem.split('.').next().unwrap_or(file_stem); + let parent = path.parent().unwrap_or(dir_path); + let second_level_output = parent.join(output_dir_name); + + build_disassembled_files_unified(BuildDisassembledFilesOptions { + file_path: &path_str, + disassembled_path: second_level_output.to_str().unwrap_or("."), + base_name: output_dir_name, + post_purge: true, + format, + unique_id_elements: Some(&rule.unique_id_elements), + strategy: "unique-id", + }) + .await?; + + if config.rules.is_empty() { + let wrap_root = parsed + .as_object() + .and_then(|o| o.keys().find(|k| *k != "?xml").cloned()) + .unwrap_or_else(|| rule.wrap_root_element.clone()); + config.rules.push(MultiLevelRule { + file_pattern: rule.file_pattern.clone(), + root_to_strip: rule.root_to_strip.clone(), + unique_id_elements: rule.unique_id_elements.clone(), + path_segment: if rule.path_segment.is_empty() { + path_segment_from_file_pattern(&rule.file_pattern) + } else { + rule.path_segment.clone() + }, + // Persist document root (e.g. LoyaltyProgramSetup) so reassembly uses it as root with xmlns; + // path_segment (e.g. programProcesses) is the inner wrapper in each file. + wrap_root_element: wrap_root, + wrap_xmlns: if rule.wrap_xmlns.is_empty() { + wrap_xmlns + } else { + rule.wrap_xmlns.clone() + }, + }); + } else if let Some(r) = config.rules.first_mut() { + if r.wrap_xmlns.is_empty() { + r.wrap_xmlns = wrap_xmlns; + } + } + } + } + } + + if !config.rules.is_empty() { + save_multi_level_config(dir_path, &config).await?; + } + + Ok(()) } } diff --git a/src/handlers/reassemble.rs b/src/handlers/reassemble.rs index a39ae15..6525f1a 100644 --- a/src/handlers/reassemble.rs +++ b/src/handlers/reassemble.rs @@ -1,13 +1,30 @@ //! Reassemble XML from disassembled directory. use crate::builders::{build_xml_string, merge_xml_elements, reorder_root_keys}; +use crate::multi_level::{ensure_segment_files_structure, load_multi_level_config}; use crate::parsers::parse_to_xml_object; use crate::types::XmlElement; +use serde_json::{Map, Value}; use std::future::Future; use std::path::Path; use std::pin::Pin; use tokio::fs; +/// Remove @xmlns from an object so the reassembled segment wrapper (e.g. programProcesses) has no xmlns. +fn strip_xmlns_from_value(v: Value) -> Value { + let obj = match v.as_object() { + Some(o) => o, + None => return v, + }; + let mut out = Map::new(); + for (k, val) in obj { + if k != "@xmlns" { + out.insert(k.clone(), val.clone()); + } + } + Value::Object(out) +} + type ProcessDirFuture<'a> = Pin< Box< dyn Future, Box>> @@ -33,9 +50,83 @@ impl ReassembleXmlFileHandler { return Ok(()); } + let path = Path::new(file_path); + let config = load_multi_level_config(path).await; + if let Some(ref config) = config { + for rule in &config.rules { + if rule.path_segment.is_empty() { + continue; + } + let segment_path = path.join(&rule.path_segment); + if !segment_path.is_dir() { + continue; + } + let mut entries = Vec::new(); + let mut read_dir = fs::read_dir(&segment_path).await?; + while let Some(entry) = read_dir.next_entry().await? { + entries.push(entry); + } + for entry in entries { + let process_path = entry.path(); + if !process_path.is_dir() { + continue; + } + let process_path_str = process_path.to_string_lossy().to_string(); + let mut sub_entries = Vec::new(); + let mut sub_read = fs::read_dir(&process_path).await?; + while let Some(e) = sub_read.next_entry().await? { + sub_entries.push(e); + } + for sub_entry in sub_entries { + let sub_path = sub_entry.path(); + if sub_path.is_dir() { + let sub_path_str = sub_path.to_string_lossy().to_string(); + self.reassemble_plain(&sub_path_str, Some("xml"), true, None) + .await?; + } + } + self.reassemble_plain(&process_path_str, Some("xml"), true, None) + .await?; + } + ensure_segment_files_structure( + &segment_path, + &rule.wrap_root_element, + &rule.path_segment, + &rule.wrap_xmlns, + ) + .await?; + } + } + + let base_segment = config.as_ref().and_then(|c| { + c.rules.first().map(|r| { + ( + file_path.to_string(), + r.path_segment.clone(), + true, // extract_inner: segment files have document_root > segment > content + ) + }) + }); + // When multi-level reassembly is done, purge the entire disassembled directory + let post_purge_final = post_purge || config.is_some(); + self.reassemble_plain(file_path, file_extension, post_purge_final, base_segment) + .await + } + + /// Merge and write reassembled XML (no multi-level pre-step). Used internally. + /// When base_segment is Some((base_path, segment_name, extract_inner)), processing that base path + /// treats the segment subdir as one key whose value is an array; when extract_inner is true, + /// each file's root has document_root > segment > content and we use content (not whole root). + async fn reassemble_plain( + &self, + file_path: &str, + file_extension: Option<&str>, + post_purge: bool, + base_segment: Option<(String, String, bool)>, + ) -> Result<(), Box> { log::debug!("Parsing directory to reassemble: {}", file_path); let parsed_objects = self - .process_files_in_directory(file_path.to_string()) + .process_files_in_directory(file_path.to_string(), base_segment) .await?; if parsed_objects.is_empty() { @@ -75,7 +166,11 @@ impl ReassembleXmlFileHandler { Ok(()) } - fn process_files_in_directory<'a>(&'a self, dir_path: String) -> ProcessDirFuture<'a> { + fn process_files_in_directory<'a>( + &'a self, + dir_path: String, + base_segment: Option<(String, String, bool)>, + ) -> ProcessDirFuture<'a> { Box::pin(async move { let mut parsed = Vec::new(); let mut entries = Vec::new(); @@ -103,6 +198,13 @@ impl ReassembleXmlFileHandler { a_base.cmp(&b_base) }); + let is_base = base_segment + .as_ref() + .map(|(base, _, _)| dir_path == *base) + .unwrap_or(false); + let segment_name = base_segment.as_ref().map(|(_, name, _)| name.as_str()); + let extract_inner = base_segment.as_ref().map(|(_, _, e)| *e).unwrap_or(false); + for entry in entries { let path = entry.path(); let file_path = path.to_string_lossy().to_string(); @@ -115,8 +217,24 @@ impl ReassembleXmlFileHandler { } } } else if path.is_dir() { - let sub_parsed = self.process_files_in_directory(file_path).await?; - parsed.extend(sub_parsed); + let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if is_base && segment_name == Some(dir_name) { + let segment_element = self + .collect_segment_as_array( + &file_path, + segment_name.unwrap(), + extract_inner, + ) + .await?; + if let Some(el) = segment_element { + parsed.push(el); + } + } else { + let sub_parsed = self + .process_files_in_directory(file_path, base_segment.clone()) + .await?; + parsed.extend(sub_parsed); + } } } @@ -124,6 +242,83 @@ impl ReassembleXmlFileHandler { }) } + /// Collect all .xml files in a directory, parse each, and build one element with + /// root_key and single key segment_name whose value is array of each file's content. + /// When extract_inner is true, each file has root > segment_name > content; we push that content. + async fn collect_segment_as_array( + &self, + segment_dir: &str, + segment_name: &str, + extract_inner: bool, + ) -> Result, Box> { + let mut xml_files = Vec::new(); + let mut read_dir = fs::read_dir(segment_dir).await?; + while let Some(entry) = read_dir.next_entry().await? { + let path = entry.path(); + if path.is_file() { + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if !name.starts_with('.') && self.is_parsable_file(name) { + xml_files.push(path.to_string_lossy().to_string()); + } + } + } + xml_files.sort(); + + let mut root_contents = Vec::new(); + let mut first_xml: Option<(String, Option)> = None; + for file_path in &xml_files { + let parsed = match parse_to_xml_object(file_path).await { + Some(p) => p, + None => continue, + }; + let obj = match parsed.as_object() { + Some(o) => o, + None => continue, + }; + let root_key = match obj.keys().find(|k| *k != "?xml").cloned() { + Some(k) => k, + None => continue, + }; + let root_val = obj + .get(&root_key) + .cloned() + .unwrap_or(Value::Object(serde_json::Map::new())); + let mut content = if extract_inner { + root_val + .get(segment_name) + .cloned() + .unwrap_or_else(|| Value::Object(serde_json::Map::new())) + } else { + root_val + }; + // Inner segment element (e.g. programProcesses) should not have xmlns in output + if extract_inner { + content = strip_xmlns_from_value(content); + } + root_contents.push(content); + if first_xml.is_none() { + first_xml = Some((root_key, obj.get("?xml").cloned())); + } + } + if root_contents.is_empty() { + return Ok(None); + } + let (root_key, decl_opt) = first_xml.unwrap(); + let mut content = serde_json::Map::new(); + content.insert(segment_name.to_string(), Value::Array(root_contents)); + let mut top = serde_json::Map::new(); + if let Some(decl) = decl_opt { + top.insert("?xml".to_string(), decl); + } else { + let mut d = serde_json::Map::new(); + d.insert("@version".to_string(), Value::String("1.0".to_string())); + d.insert("@encoding".to_string(), Value::String("UTF-8".to_string())); + top.insert("?xml".to_string(), Value::Object(d)); + } + top.insert(root_key, Value::Object(content)); + Ok(Some(Value::Object(top))) + } + fn is_parsable_file(&self, file_name: &str) -> bool { let lower = file_name.to_lowercase(); lower.ends_with(".xml") diff --git a/src/lib.rs b/src/lib.rs index d8cfb91..ee3d416 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub mod builders; pub mod constants; pub mod handlers; +pub mod multi_level; pub mod parsers; pub mod transformers; pub mod types; @@ -10,6 +11,10 @@ pub mod utils; pub use builders::build_xml_string; pub use handlers::{DisassembleXmlFileHandler, ReassembleXmlFileHandler}; +pub use multi_level::{ + load_multi_level_config, path_segment_from_file_pattern, save_multi_level_config, + strip_root_and_build_xml, +}; pub use parsers::parse_xml; pub use transformers::{transform_to_json, transform_to_json5, transform_to_yaml}; -pub use types::XmlElement; +pub use types::{MultiLevelConfig, MultiLevelRule, XmlElement}; diff --git a/src/main.rs b/src/main.rs index bff3879..ffa2c49 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,8 @@ use std::env; use xml_disassembler::{ - build_xml_string, parse_xml, DisassembleXmlFileHandler, ReassembleXmlFileHandler, + build_xml_string, parse_xml, DisassembleXmlFileHandler, MultiLevelRule, + ReassembleXmlFileHandler, }; /// Options parsed from disassemble CLI args. @@ -14,6 +15,29 @@ struct DisassembleOpts<'a> { ignore_path: &'a str, format: &'a str, strategy: Option<&'a str>, + multi_level: Option, +} + +/// Parse --multi-level spec: "file_pattern:root_to_strip:unique_id_elements" +/// e.g. "programProcesses-meta:LoyaltyProgramSetup:parameterName,ruleName" +fn parse_multi_level_spec(spec: &str) -> Option { + let parts: Vec<&str> = spec.splitn(3, ':').collect(); + if parts.len() != 3 { + return None; + } + let (file_pattern, root_to_strip, unique_id_elements) = (parts[0], parts[1], parts[2]); + if file_pattern.is_empty() || root_to_strip.is_empty() || unique_id_elements.is_empty() { + return None; + } + let path_segment = xml_disassembler::path_segment_from_file_pattern(file_pattern); + Some(MultiLevelRule { + file_pattern: file_pattern.to_string(), + root_to_strip: root_to_strip.to_string(), + unique_id_elements: unique_id_elements.to_string(), + path_segment: path_segment.clone(), + wrap_root_element: root_to_strip.to_string(), + wrap_xmlns: String::new(), + }) } /// Parse disassemble args: [options]. @@ -26,6 +50,7 @@ fn parse_disassemble_args(args: &[String]) -> DisassembleOpts<'_> { let mut ignore_path = ".xmldisassemblerignore"; let mut format = "xml"; let mut strategy = None; + let mut multi_level = None; let mut i = 0; while i < args.len() { @@ -72,6 +97,15 @@ fn parse_disassemble_args(args: &[String]) -> DisassembleOpts<'_> { strategy = Some(args[i].as_str()); i += 1; } + } else if arg.starts_with("--multi-level=") { + multi_level = Some(arg.trim_start_matches("--multi-level=").to_string()); + i += 1; + } else if arg == "--multi-level" { + i += 1; + if i < args.len() { + multi_level = Some(args[i].clone()); + i += 1; + } } else if arg.starts_with("--") { i += 1; } else if path.is_none() { @@ -90,6 +124,7 @@ fn parse_disassemble_args(args: &[String]) -> DisassembleOpts<'_> { ignore_path, format, strategy, + multi_level, } } @@ -127,6 +162,7 @@ async fn main() -> Result<(), Box> { eprintln!( " --strategy - unique-id or grouped-by-tag (default: unique-id)" ); + eprintln!(" --multi-level - Further disassemble matching files: file_pattern:root_to_strip:unique_id_elements"); eprintln!(" reassemble [extension] [--postpurge] - Reassemble directory (default extension: xml)"); eprintln!(" parse - Parse and rebuild XML (test)"); return Ok(()); @@ -139,6 +175,15 @@ async fn main() -> Result<(), Box> { "disassemble" => { let opts = parse_disassemble_args(&args[2..]); let path = opts.path.unwrap_or("."); + let multi_level_rule = opts + .multi_level + .as_ref() + .and_then(|s| parse_multi_level_spec(s)); + if opts.multi_level.is_some() && multi_level_rule.is_none() { + eprintln!( + "Invalid --multi-level spec; use file_pattern:root_to_strip:unique_id_elements" + ); + } let mut handler = DisassembleXmlFileHandler::new(); handler .disassemble( @@ -149,6 +194,7 @@ async fn main() -> Result<(), Box> { opts.post_purge, opts.ignore_path, opts.format, + multi_level_rule.as_ref(), ) .await?; } diff --git a/src/multi_level.rs b/src/multi_level.rs new file mode 100644 index 0000000..c898ef5 --- /dev/null +++ b/src/multi_level.rs @@ -0,0 +1,222 @@ +//! Multi-level disassembly: strip a root element and re-disassemble with different unique-id elements. + +use serde_json::{Map, Value}; + +use crate::builders::build_xml_string; +use crate::types::{MultiLevelConfig, XmlElement}; + +/// Strip the given element and build a new XML string. +/// - If it is the root element: its inner content becomes the new document (with ?xml preserved). +/// - If it is a child of the root (e.g. programProcesses under LoyaltyProgramSetup): unwrap it so +/// its inner content becomes the direct children of the root; the root element is kept. +pub fn strip_root_and_build_xml(parsed: &XmlElement, element_to_strip: &str) -> Option { + let obj = parsed.as_object()?; + let root_key = obj.keys().find(|k| *k != "?xml")?.clone(); + let root_val = obj.get(&root_key)?.as_object()?; + let decl = obj.get("?xml").cloned().unwrap_or_else(|| { + let mut d = Map::new(); + d.insert("@version".to_string(), Value::String("1.0".to_string())); + d.insert("@encoding".to_string(), Value::String("UTF-8".to_string())); + Value::Object(d) + }); + + if root_key == element_to_strip { + // Strip the root: new doc = ?xml + inner content of root + let mut new_obj = Map::new(); + new_obj.insert("?xml".to_string(), decl); + for (k, v) in root_val { + new_obj.insert(k.clone(), v.clone()); + } + return Some(build_xml_string(&Value::Object(new_obj))); + } + + // Strip a child of the root: unwrap it so its inner content becomes direct children of the root + let inner = root_val.get(element_to_strip)?.as_object()?; + let mut new_root_val = Map::new(); + for (k, v) in root_val { + if k != element_to_strip { + new_root_val.insert(k.clone(), v.clone()); + } + } + for (k, v) in inner { + new_root_val.insert(k.clone(), v.clone()); + } + let mut new_obj = Map::new(); + new_obj.insert("?xml".to_string(), decl); + new_obj.insert(root_key, Value::Object(new_root_val)); + Some(build_xml_string(&Value::Object(new_obj))) +} + +/// Capture xmlns from the root element (e.g. LoyaltyProgramSetup) for later wrap. +pub fn capture_xmlns_from_root(parsed: &XmlElement) -> Option { + let obj = parsed.as_object()?; + let root_key = obj.keys().find(|k| *k != "?xml")?.clone(); + let root_val = obj.get(&root_key)?.as_object()?; + let xmlns = root_val.get("@xmlns")?.as_str()?; + Some(xmlns.to_string()) +} + +/// Derive path_segment from file_pattern (e.g. "programProcesses-meta" -> "programProcesses"). +pub fn path_segment_from_file_pattern(file_pattern: &str) -> String { + if let Some(prefix) = file_pattern.split('-').next() { + prefix.to_string() + } else { + file_pattern.to_string() + } +} + +/// Load multi-level config from a directory (reads .multi_level.json). +pub async fn load_multi_level_config(dir_path: &std::path::Path) -> Option { + let path = dir_path.join(".multi_level.json"); + let content = tokio::fs::read_to_string(&path).await.ok()?; + serde_json::from_str(&content).ok() +} + +/// Persist multi-level config to a directory. +pub async fn save_multi_level_config( + dir_path: &std::path::Path, + config: &MultiLevelConfig, +) -> Result<(), Box> { + let path = dir_path.join(".multi_level.json"); + let content = serde_json::to_string_pretty(config)?; + tokio::fs::write(path, content).await?; + Ok(()) +} + +/// Ensure all XML files in a segment directory have structure: +/// document_root (with xmlns) > inner_wrapper (no xmlns) > content. +/// Used after inner-level reassembly for multi-level (e.g. LoyaltyProgramSetup > programProcesses). +pub async fn ensure_segment_files_structure( + dir_path: &std::path::Path, + document_root: &str, + inner_wrapper: &str, + xmlns: &str, +) -> Result<(), Box> { + use crate::parsers::parse_xml_from_str; + use serde_json::Map; + + let mut entries = Vec::new(); + let mut read_dir = tokio::fs::read_dir(dir_path).await?; + while let Some(entry) = read_dir.next_entry().await? { + entries.push(entry); + } + + for entry in entries { + let path = entry.path(); + if !path.is_file() { + continue; + } + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if !name.ends_with(".xml") { + continue; + } + let path_str = path.to_string_lossy(); + let content = match tokio::fs::read_to_string(&path).await { + Ok(c) => c, + Err(_) => continue, + }; + let parsed = match parse_xml_from_str(&content, &path_str) { + Some(p) => p, + None => continue, + }; + let obj = match parsed.as_object() { + Some(o) => o, + None => continue, + }; + let root_key = obj.keys().find(|k| *k != "?xml").cloned(); + let Some(current_root_key) = root_key else { + continue; + }; + let root_val = obj + .get(¤t_root_key) + .and_then(|v| v.as_object()) + .cloned(); + let Some(root_val) = root_val else { + continue; + }; + + let decl = obj.get("?xml").cloned().unwrap_or_else(|| { + let mut d = Map::new(); + d.insert( + "@version".to_string(), + serde_json::Value::String("1.0".to_string()), + ); + d.insert( + "@encoding".to_string(), + serde_json::Value::String("UTF-8".to_string()), + ); + serde_json::Value::Object(d) + }); + + let non_attr_keys: Vec<&String> = root_val.keys().filter(|k| *k != "@xmlns").collect(); + let single_inner = non_attr_keys.len() == 1 && non_attr_keys[0].as_str() == inner_wrapper; + let inner_content: serde_json::Value = if current_root_key == document_root && single_inner + { + let inner_obj = root_val + .get(inner_wrapper) + .and_then(|v| v.as_object()) + .cloned() + .unwrap_or_else(Map::new); + let mut inner_clean = Map::new(); + for (k, v) in &inner_obj { + if k != "@xmlns" { + inner_clean.insert(k.clone(), v.clone()); + } + } + serde_json::Value::Object(inner_clean) + } else { + serde_json::Value::Object(root_val.clone()) + }; + + let already_correct = current_root_key == document_root + && root_val.get("@xmlns").is_some() + && single_inner + && root_val + .get(inner_wrapper) + .and_then(|v| v.as_object()) + .map(|o| !o.contains_key("@xmlns")) + .unwrap_or(true); + if already_correct { + continue; + } + + // Build document_root (with @xmlns only on root) > inner_wrapper (no xmlns) > content + let mut root_val_new = Map::new(); + if !xmlns.is_empty() { + root_val_new.insert( + "@xmlns".to_string(), + serde_json::Value::String(xmlns.to_string()), + ); + } + root_val_new.insert(inner_wrapper.to_string(), inner_content); + + let mut top = Map::new(); + top.insert("?xml".to_string(), decl); + top.insert( + document_root.to_string(), + serde_json::Value::Object(root_val_new), + ); + let wrapped = serde_json::Value::Object(top); + let xml_string = build_xml_string(&wrapped); + tokio::fs::write(&path, xml_string).await?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn path_segment_from_file_pattern_strips_suffix() { + assert_eq!( + path_segment_from_file_pattern("programProcesses-meta"), + "programProcesses" + ); + } + + #[test] + fn path_segment_from_file_pattern_no_dash() { + assert_eq!(path_segment_from_file_pattern("foo"), "foo"); + } +} diff --git a/src/types.rs b/src/types.rs index b19cad1..e3f992f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -86,3 +86,29 @@ pub struct LeafWriteOptions<'a> { pub xml_declaration: Option, pub format: &'a str, } + +/// Rule for multi-level disassembly: which files to further disassemble and how. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct MultiLevelRule { + /// File name pattern (e.g. "programProcesses-meta"); any XML file whose name contains this is processed. + pub file_pattern: String, + /// Root element to strip (e.g. "LoyaltyProgramSetup"); its inner content becomes the new document. + pub root_to_strip: String, + /// Comma-separated unique-id elements for the second-level disassembly (e.g. "parameterName,ruleName"). + pub unique_id_elements: String, + /// Path segment under the disassembly root for reassembly (e.g. "programProcesses"). + #[serde(default)] + pub path_segment: String, + /// Root element name to wrap reassembled files with (defaults to root_to_strip). + #[serde(default)] + pub wrap_root_element: String, + /// xmlns value for the wrap root (optional; captured from original when stripping). + #[serde(default)] + pub wrap_xmlns: String, +} + +/// Persisted config for multi-level reassembly (stored as .multi_level.json in the disassembly root). +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +pub struct MultiLevelConfig { + pub rules: Vec, +} diff --git a/tests/disassemble_reassemble.rs b/tests/disassemble_reassemble.rs index eae06c8..4477b51 100644 --- a/tests/disassemble_reassemble.rs +++ b/tests/disassemble_reassemble.rs @@ -36,6 +36,7 @@ async fn disassemble_then_reassemble_matches_original_xml() { false, ".xmldisassemblerignore", "xml", + None, ) .await .expect("disassemble"); @@ -92,6 +93,7 @@ async fn cdata_preserved_round_trip() { false, ".xmldisassemblerignore", "xml", + None, ) .await .expect("disassemble"); @@ -152,6 +154,7 @@ async fn comments_preserved_round_trip() { false, ".xmldisassemblerignore", "xml", + None, ) .await .expect("disassemble"); @@ -215,6 +218,7 @@ async fn deeply_nested_unique_id_elements_round_trip() { false, ".xmldisassemblerignore", "xml", + None, ) .await .expect("disassemble"); @@ -244,3 +248,71 @@ async fn deeply_nested_unique_id_elements_round_trip() { "Reassembled XML must match original (deeply nested unique ID elements round-trip)" ); } + +/// Multi-level disassembly: first disassemble by processName etc., then further disassemble +/// programProcesses by parameterName and ruleName. Reassemble and compare to original. +#[tokio::test] +async fn multi_level_disassemble_then_reassemble_matches_original() { + let _ = env_logger::try_init(); + + let fixture = "fixtures/multi-level/Cloud_Kicks_Inner_Circle.loyaltyProgramSetup-meta.xml"; + assert!( + Path::new(fixture).exists(), + "Fixture {} must exist (run from project root)", + fixture + ); + + let original_content = std::fs::read_to_string(fixture).expect("read original fixture"); + + let temp_dir = tempfile::tempdir().expect("temp dir"); + let base = temp_dir.path(); + let disassembled_dir = base.join("Cloud_Kicks_Inner_Circle"); + + let source_in_temp = base.join("Cloud_Kicks_Inner_Circle.loyaltyProgramSetup-meta.xml"); + std::fs::copy(fixture, &source_in_temp).expect("copy fixture to temp"); + + let rule = xml_disassembler::MultiLevelRule { + file_pattern: "programProcesses".to_string(), + root_to_strip: "programProcesses".to_string(), + unique_id_elements: "parameterName,ruleName".to_string(), + path_segment: "programProcesses".to_string(), + wrap_root_element: "LoyaltyProgramSetup".to_string(), + wrap_xmlns: String::new(), + }; + + let mut disassemble = DisassembleXmlFileHandler::new(); + disassemble + .disassemble( + source_in_temp.to_str().unwrap(), + Some("fullName,name,processName"), + Some("unique-id"), + false, + false, + ".xmldisassemblerignore", + "xml", + Some(&rule), + ) + .await + .expect("disassemble"); + + assert!( + disassembled_dir.exists(), + "Disassembled directory should exist" + ); + + let reassemble_handler = ReassembleXmlFileHandler::new(); + reassemble_handler + .reassemble(disassembled_dir.to_str().unwrap(), Some("xml"), false) + .await + .expect("reassemble"); + + let reassembled_path = base.join("Cloud_Kicks_Inner_Circle.xml"); + assert!(reassembled_path.exists(), "Reassembled file should exist"); + + let reassembled_content = std::fs::read_to_string(&reassembled_path).expect("read reassembled"); + + assert_eq!( + original_content, reassembled_content, + "Reassembled XML must match original (multi-level round-trip)" + ); +}