diff --git a/.gitignore b/.gitignore index 70c1d27f..1247e42d 100644 --- a/.gitignore +++ b/.gitignore @@ -115,4 +115,7 @@ composer.phar composer.lock # Build -build.sh \ No newline at end of file +build.sh + +# Claude Code +.claude \ No newline at end of file diff --git a/assets/js/payment-forms.js b/assets/js/payment-forms.js index d3377905..69f89e22 100644 --- a/assets/js/payment-forms.js +++ b/assets/js/payment-forms.js @@ -496,6 +496,38 @@ jQuery(function ($) { } }); + // Authorize.net ACH/Card payment type toggle. + this.form.on('change', '.getpaid-authorizenet-payment-type-selector input[type="radio"]', function () { + var paymentType = $(this).val(); + var container = $(this).closest('.getpaid-gateway-description'); + + if (paymentType === 'ach') { + container.find('.getpaid-authorizenet-cc-section').hide(); + container.find('.getpaid-authorizenet-ach-section').show(); + } else { + container.find('.getpaid-authorizenet-cc-section').show(); + container.find('.getpaid-authorizenet-ach-section').hide(); + } + }); + + // Format ACH routing number on input (9 digits only). + this.form.on('input', '.getpaid-ach-routing-number', function () { + var input = $(this); + var value = input.val().replace(/\D/g, '').substring(0, 9); + if (input.val() !== value) { + input.val(value); + } + }); + + // Format ACH account number on input (17 digits max). + this.form.on('input', '.getpaid-ach-account-number', function () { + var input = $(this); + var value = input.val().replace(/\D/g, '').substring(0, 17); + if (input.val() !== value) { + input.val(value); + } + }); + // Discounts. if (this.form.find('.getpaid-discount-field').length) { diff --git a/assets/js/payment-forms.min.js b/assets/js/payment-forms.min.js index 95a309ae..69c6ecda 100644 --- a/assets/js/payment-forms.min.js +++ b/assets/js/payment-forms.min.js @@ -1 +1 @@ -jQuery(function($){var funcs=[];var isRecaptchaLoaded=false;function excecute_after_recaptcha_load(func){if(isRecaptchaLoaded){func();return}funcs.push(func);var interval=setInterval(function(){if(typeof grecaptcha!=="undefined"){clearInterval(interval);isRecaptchaLoaded=true;funcs.forEach(function(func){func()})}},100)}function gp_throttle(callback,limit){if(!limit){limit=200}var wait=false;var did_last=true;return function(){if(!wait){did_last=true;callback.bind(this).call();wait=true;setTimeout(function(){wait=false},limit)}else{did_last=false;var that=this;setTimeout(function(){if(!did_last){callback.bind(that).call();did_last=true}},limit)}}}window.getpaid_form=function(form){return{fetched_initial_state:0,cached_states:{},form:form,show_error(error,container){form.find(".getpaid-payment-form-errors, .getpaid-custom-payment-form-errors").html("").addClass("d-none");if(container&&form.find(container).length){form.find(container).html(error).removeClass("d-none");form.find(container).closest(".form-group").find(".form-control").addClass("is-invalid");form.find(container).closest(".form-group").find(".getpaid-custom-payment-form-success").addClass("d-none")}else{form.find(".getpaid-payment-form-errors").html(error).removeClass("d-none");form.find(".getpaid-custom-payment-form-errors").each(function(){var form_control=$(this).closest(".form-group").find(".form-control");if(form_control.val()!=""){form_control.addClass("is-valid")}})}},hide_error(){form.find(".getpaid-payment-form-errors, .getpaid-custom-payment-form-errors").html("").addClass("d-none");form.find(".is-invalid, .is-valid").removeClass("is-invalid is-valid")},cache_state(key,state){this.cached_states[key]=state},current_state_key(){return this.form.serialize()},is_current_state_cached(){return this.cached_states.hasOwnProperty(this.current_state_key())},switch_state(){this.hide_error();var state=this.cached_states[this.current_state_key()];if(!state){return this.fetch_state()}var template=this.form.find(".getpaid-tax-template");if(state.totals){for(var total in state.totals){if(state.totals.hasOwnProperty(total)){this.form.find(".getpaid-form-cart-totals-total-"+total).html(state.totals[total])}}}if(template.length){this.form.find(".getpaid-form-cart-totals-tax:not(.getpaid-tax-template)").remove();if(!Array.isArray(state.taxes)){var html=template.prop("outerHTML");for(var tax in state.taxes){if(state.taxes.hasOwnProperty(tax)){var newTax=template.before(html).prev().removeClass("getpaid-tax-template d-none");newTax.find(".getpaid-payment-form-line-totals-label").html(tax);newTax.find(".getpaid-payment-form-line-totals-value").html(state.taxes[tax])}}}}if(!Array.isArray(state.fees)){this.form.find(".getpaid-form-cart-totals-fees").removeClass("d-none")}else{this.form.find(".getpaid-form-cart-totals-fees").addClass("d-none")}if(!Array.isArray(state.discounts)){this.form.find(".getpaid-form-cart-totals-discount").removeClass("d-none")}else{this.form.find(".getpaid-form-cart-totals-discount").addClass("d-none")}if(state.items){for(var item in state.items){if(state.items.hasOwnProperty(item)){this.form.find(".getpaid-form-cart-item-subtotal-"+item).html(state.items[item])}}}if(state.selected_items){for(var item in state.selected_items){if(state.selected_items.hasOwnProperty(item)){this.form.find('input[name="getpaid-items['+item+'][price]"]').val(state.selected_items[item].price);this.form.find('input[name="getpaid-items['+item+'][quantity]"]').val(state.selected_items[item].quantity);this.form.find(".getpaid-items-"+item+"-view-price").html(state.selected_items[item].price_fmt);this.form.find(".getpaid-items-"+item+"-view-quantity").text(state.selected_items[item].quantity)}}}if(state.texts){for(var selector in state.texts){if(state.texts.hasOwnProperty(selector)){this.form.find(selector).html(state.texts[selector])}}}if(state.gateways){this.process_gateways(state.gateways,state)}if(state.invoice){if(this.form.find('input[name="invoice_id"]').length==0){this.form.append('')}this.form.find('input[name="invoice_id"]').val(state.invoice)}if(state.js_data){this.form.data("getpaid_js_data",state.js_data)}form.find(".getpaid-custom-payment-form-errors.d-none").each(function(){var form_control=$(this).closest(".form-group").find(".form-control");if(form_control.val()!=""){form_control.addClass("is-valid").closest(".form-group").find(".getpaid-custom-payment-form-success").removeClass("d-none")}});this.setup_saved_payment_tokens();this.form.trigger("getpaid_payment_form_changed_state",[state])},refresh_state(){if(this.is_current_state_cached()){return this.switch_state()}this.fetch_state()},fetch_state(){wpinvBlock(this.form);var key=this.current_state_key();var maybe_use_invoice=this.fetched_initial_state?"0":localStorage.getItem("getpaid_last_invoice_"+this.form.find('input[name="form_id"]').val());return $.post(WPInv.ajax_url,key+"&action=wpinv_payment_form_refresh_prices&_ajax_nonce="+WPInv.formNonce+"&initial_state="+this.fetched_initial_state+"&maybe_use_invoice="+maybe_use_invoice).done(res=>{if(res.success){this.fetched_initial_state=1;this.cache_state(key,res.data);return this.switch_state()}if(res.success===false){this.show_error(res.data.error,res.data.code);return}this.show_error(res)}).fail(()=>{this.show_error(WPInv.connectionError)}).always(()=>{wpinvUnblock(this.form)})},update_state_field(wrapper){wrapper=$(wrapper);if(wrapper.find(".wpinv_state").length){var state=wrapper.find(".getpaid-address-field-wrapper__state");wpinvBlock(state);var data={action:"wpinv_get_payment_form_states_field",country:wrapper.find(".wpinv_country").val(),form:this.form.find('input[name="form_id"]').val(),name:state.find(".wpinv_state").attr("name"),_ajax_nonce:WPInv.formNonce};$.get(WPInv.ajax_url,data,res=>{if("object"==typeof res){state.replaceWith(res.data)}}).always(()=>{wpinvUnblock(wrapper.find(".getpaid-address-field-wrapper__state"))})}},attach_events(){var that=this;var on_field_change=gp_throttle(function(){that.refresh_state()},500);this.form.on("change",".getpaid-refresh-on-change",on_field_change);this.form.on("input",".getpaid-payment-form-element-price_select :input:not(.getpaid-refresh-on-change)",on_field_change);this.form.on("change",".getpaid-payment-form-element-currency_select :input:not(.getpaid-refresh-on-change)",on_field_change);this.form.on("change",".getpaid-item-quantity-input",on_field_change);this.form.on("change",'[name="getpaid-payment-form-selected-item"]',on_field_change);this.form.on("change",".getpaid-item-mobile-quantity-input",function(){let input=$(this);input.closest(".getpaid-payment-form-items-cart-item").find(".getpaid-item-quantity-input").val(input.val()).trigger("change")});this.form.on("change",".getpaid-item-quantity-input",function(){let input=$(this);input.closest(".getpaid-payment-form-items-cart-item").find(".getpaid-item-mobile-quantity-input").val(input.val())});this.form.on("change",".getpaid-item-price-input",function(){if(!$(this).hasClass("is-invalid")){on_field_change()}});this.form.on("keypress",".getpaid-refresh-on-change, .getpaid-payment-form-element-price_select :input:not(.getpaid-refresh-on-change), .getpaid-item-quantity-input, .getpaid-item-price-input",function(e){if(e.keyCode=="13"){e.preventDefault();on_field_change()}});this.form.on("change",".getpaid-shipping-address-wrapper .wpinv_country",()=>{this.update_state_field(".getpaid-shipping-address-wrapper")});this.form.on("change",".getpaid-billing-address-wrapper .wpinv_country",()=>{this.update_state_field(".getpaid-billing-address-wrapper");if(this.form.find(".getpaid-billing-address-wrapper .wpinv_country").val()!=this.form.find(".getpaid-billing-address-wrapper .wpinv_country").data("ipCountry")){this.form.find(".getpaid-address-field-wrapper__address-confirm").removeClass("d-none")}else{this.form.find(".getpaid-address-field-wrapper__address-confirm").addClass("d-none")}on_field_change()});this.form.on("change",".getpaid-billing-address-wrapper .wpinv_state, .getpaid-billing-address-wrapper .wpinv_vat_number",()=>{on_field_change()});this.form.on("click",".getpaid-vat-number-validate",e=>{e.preventDefault();this.validate_vat_number()});this.form.on("click",'[name="confirm-address"]',()=>{on_field_change()});this.form.on("change",".getpaid-billing-address-wrapper .wpinv_vat_number",function(){var validator=$(this).parent().find(".getpaid-vat-number-validate");validator.text(validator.data("validate"))});this.form.on("input",".getpaid-format-card-number",function(){var input=$(this);var value=input.val();var formatted=value.replace(/\D/g,"").replace(/(.{4})/g,"$1 ");if(value!=formatted){input.val(formatted)}});if(this.form.find(".getpaid-discount-field").length){this.form.find(".getpaid-discount-button").on("click",function(e){e.preventDefault();on_field_change()});this.form.find(".getpaid-discount-field").on("keypress",function(e){if(e.keyCode=="13"){e.preventDefault();on_field_change()}});this.form.find(".getpaid-discount-field").on("change",function(e){on_field_change()})}this.form.on("change",".getpaid-gateway-radio input",()=>{var gateway=this.form.find(".getpaid-gateway-radio input:checked").val();form.find(".getpaid-gateway-description").slideUp();form.find(`.getpaid-description-${gateway}`).slideDown()});this.form.find(".getpaid-file-upload-element").each(function(){let dropfield=$(this),parent=dropfield.closest(".upload-file")||dropfield.closest(".form-group"),uploaded_files_div=parent.find(".getpaid-uploaded-files"),max_files=parseInt(parent.data("max")||1);let loadedFiles=[],xhr;let loadFile=function(file_data){if(!file_data){return}let progress_bar=parent.find(".getpaid-progress-template").clone().removeClass("d-none getpaid-progress-template");uploaded_files_div.append(progress_bar);progress_bar.find("a.close").on("click",function(e){e.preventDefault();let index=loadedFiles.indexOf(file_data);if(index>-1){loadedFiles=loadedFiles.splice(index,1)}progress_bar.fadeOut(300,()=>{progress_bar.remove()});try{if(xhr){xhr.abort()}}catch(e){}});progress_bar.find(".getpaid-progress-file-name").text(file_data.name).attr("title",file_data.name);progress_bar.find(".progress-bar").attr("aria-valuemax",file_data.size);let icons={"application/pdf":'',"application/zip":'',"application/x-gzip":'',"application/rar":'',"application/x-7z-compressed":'',"application/x-tar":'',audio:'',image:'',video:'',"application/msword":'',"application/vnd.ms-excel":'',"application/msword":'',"application/vnd.ms-word":'',"application/vnd.ms-powerpoint":''};if(file_data.type){Object.keys(icons).forEach(function(prop){if(file_data.type.indexOf(prop)!==-1){progress_bar.find(".fa.fa-file").replaceWith(icons[prop])}})}if(max_files&&loadedFiles.length>=max_files){progress_bar.find(".getpaid-progress").html('");return}let extension=file_data.name.match(/\.([^\.]+)$/)[1],extensions=parent.find(".getpaid-files-input").data("extensions");if(extensions.indexOf(extension.toString().toLowerCase())<0){progress_bar.find(".getpaid-progress").html('");return}let form_data=new FormData;form_data.append("file",file_data);form_data.append("action","wpinv_file_upload");form_data.append("form_id",progress_bar.closest("form").find('input[name="form_id"]').val());form_data.append("_ajax_nonce",WPInv.formNonce);form_data.append("field_name",parent.data("name"));loadedFiles.push(file_data);xhr=$.ajax({url:WPInv.ajax_url,type:"POST",contentType:false,processData:false,data:form_data,xhr:function(){let _xhr=new window.XMLHttpRequest;_xhr.upload.addEventListener("progress",function(e){if(e.lengthComputable){let percentLoaded=Math.round(e.loaded*100/e.total)+"%";progress_bar.find(".progress-bar").attr("aria-valuenow",e.loaded).css("width",percentLoaded).text(percentLoaded)}},false);return _xhr},success:function(response){if(response.success){progress_bar.append(response.data)}else{progress_bar.find(".getpaid-progress").html('")}},error:function(request,status,message){progress_bar.find(".getpaid-progress").html('")}})};let loadFiles=function loadFiles(flist){Array.prototype.forEach.apply(flist,[loadFile])};dropfield.on("dragenter",()=>{dropfield.addClass("getpaid-trying-to-drop")}).on("dragover",e=>{e=e.originalEvent;e.stopPropagation();e.preventDefault();e.dataTransfer.dropEffect="copy"}).on("dragleave",()=>{dropfield.removeClass("getpaid-trying-to-drop")}).on("drop",e=>{e=e.originalEvent;e.stopPropagation();e.preventDefault();let files=e.dataTransfer.files;if(files.length>0){loadFiles(files)}});parent.find(".getpaid-files-input").on("change",function(e){let files=e.originalEvent.target.files;if(files){loadFiles(files);parent.find(".getpaid-files-input").val("")}})});if(jQuery.fn.popover&&this.form.find(".gp-tooltip").length){this.form.find(".gp-tooltip").popover({container:this.form[0],html:true,content:function(){return $(this).closest(".getpaid-form-cart-item-name").find(".getpaid-item-desc").html()}})}if(jQuery.fn.flatpickr&&this.form.find(".getpaid-init-flatpickr").length){this.form.find(".getpaid-init-flatpickr").each(function(){let options={},$el=jQuery(this);if($el.data("disable_alt")&&$el.data("disable_alt").length>0){options.disable=$el.data("disable_alt")}if($el.data("disable_days_alt")&&$el.data("disable_days_alt").length>0){options.disable=options.disable||[];let disabled_days=$el.data("disable_days_alt");options.disable.push(function(date){return disabled_days.indexOf(date.getDay())>=0})}jQuery(this).removeClass("flatpickr-input").flatpickr(options)})}if(WPInv.recaptchaSettings&&WPInv.recaptchaSettings.enabled&&WPInv.recaptchaSettings.version=="v2"){excecute_after_recaptcha_load(()=>{var id=this.form.find(".getpaid-recaptcha-wrapper .g-recaptcha").attr("id");if(id){grecaptcha.ready(()=>{grecaptcha.render(id,WPInv.recaptchaSettings.render_params)})}})}},process_gateways(enabled_gateways,state){this.form.data("initial_amt",state.initial_amt);this.form.data("currency",state.currency);var submit_btn=this.form.find(".getpaid-payment-form-submit");var free_label=submit_btn.data("free").replace(/%price%/gi,state.totals.raw_total);var btn_label=submit_btn.data("pay").replace(/%price%/gi,state.totals.raw_total);submit_btn.prop("disabled",false).css("cursor","pointer");if(state.is_free){submit_btn.val(free_label);this.form.find(".getpaid-gateways").slideUp();this.form.data("isFree","yes");return}this.form.data("isFree","no");this.form.find(".getpaid-gateways").slideDown();submit_btn.val(btn_label);this.form.find(".getpaid-no-recurring-gateways, .getpaid-no-subscription-group-gateways, .getpaid-no-multiple-subscription-group-gateways, .getpaid-no-active-gateways").addClass("d-none");this.form.find(".getpaid-select-gateway-title-div, .getpaid-available-gateways-div, .getpaid-gateway-descriptions-div").removeClass("d-none");if(enabled_gateways.length<1){this.form.find(".getpaid-select-gateway-title-div, .getpaid-available-gateways-div, .getpaid-gateway-descriptions-div").addClass("d-none");submit_btn.prop("disabled",true).css("cursor","not-allowed");if(state.has_multiple_subscription_groups){this.form.find(".getpaid-no-multiple-subscription-group-gateways").removeClass("d-none");return}if(state.has_subscription_group){this.form.find(".getpaid-no-subscription-group-gateways").removeClass("d-none");return}if(state.has_recurring){this.form.find(".getpaid-no-recurring-gateways").removeClass("d-none");return}this.form.find(".getpaid-no-active-gateways").removeClass("d-none");return}if(enabled_gateways.length==1){this.form.find(".getpaid-select-gateway-title-div").addClass("d-none");this.form.find(".getpaid-gateway-radio input").addClass("d-none")}else{this.form.find(".getpaid-gateway-radio input").removeClass("d-none")}this.form.find(".getpaid-gateway").addClass("d-none");$.each(enabled_gateways,(index,value)=>{this.form.find(`.getpaid-gateway-${value}`).removeClass("d-none")});if(0===this.form.find(".getpaid-gateway:visible input:checked").length){this.form.find(".getpaid-gateway:visible .getpaid-gateway-radio input").eq(0).prop("checked",true)}if(0===this.form.find(".getpaid-gateway-description:visible").length){this.form.find(".getpaid-gateway-radio input:checked").trigger("change")}},setup_saved_payment_tokens(){var currency=this.form.data("currency");this.form.find(".getpaid-saved-payment-methods").each(function(){var list=$(this);list.show();$("input",list).on("change",function(){if($(this).closest("li").hasClass("getpaid-new-payment-method")){list.closest(".getpaid-gateway-description").find(".getpaid-new-payment-method-form").slideDown()}else{list.closest(".getpaid-gateway-description").find(".getpaid-new-payment-method-form").slideUp()}});list.find("input").each(function(){if("none"!=$(this).data("currency")&¤cy!=$(this).data("currency")){$(this).closest("li").addClass("d-none");$(this).prop("checked",false)}else{$(this).closest("li").removeClass("d-none")}});if(0===$("li:not(.d-none) input",list).filter(":checked").length){$("li:not(.d-none) input",list).eq(0).prop("checked",true)}if(0===$("li:not(.d-none) input",list).filter(":checked").length){$("input",list).last().prop("checked",true)}if(2>$("li:not(.d-none) input",list).length){list.hide()}$("input",list).filter(":checked").trigger("change")})},handleAddressToggle(address_toggle){var wrapper=address_toggle.closest(".getpaid-payment-form-element-address");wrapper.find(".getpaid-billing-address-title, .getpaid-shipping-address-title, .getpaid-shipping-address-wrapper").addClass("d-none");address_toggle.on("change",function(){if($(this).is(":checked")){wrapper.find(".getpaid-billing-address-title, .getpaid-shipping-address-title, .getpaid-shipping-address-wrapper").addClass("d-none");wrapper.find(".getpaid-shipping-billing-address-title").removeClass("d-none")}else{wrapper.find(".getpaid-billing-address-title, .getpaid-shipping-address-title, .getpaid-shipping-address-wrapper").removeClass("d-none");wrapper.find(".getpaid-shipping-billing-address-title").addClass("d-none")}})},validate_vat_number(){var vat_input=this.form.find(".wpinv_vat_number");var country_input=this.form.find(".wpinv_country");var validator=vat_input.parent().find(".getpaid-vat-number-validate");if(!vat_input.length||!country_input.length){return}var vat_number=vat_input.val();var country=country_input.val();if(!vat_number||!country){this.show_error(WPInv.vatFieldsRequired,".getpaid-error-billingwpinv_vat_number");return}validator.prop("disabled",true).text(WPInv.validating);$.post(WPInv.ajax_url,{action:"wpinv_validate_vat_number",vat_number:vat_number,country:country,_ajax_nonce:WPInv.formNonce}).done(response=>{if(response.success){validator.removeClass("btn-primary").addClass("btn-success").text(WPInv.valid);vat_input.removeClass("is-invalid").addClass("is-valid");this.hide_error()}else{validator.removeClass("btn-success").addClass("btn-danger").text(WPInv.invalid);vat_input.removeClass("is-valid").addClass("is-invalid");this.show_error(response.data.message,".getpaid-error-billingwpinv_vat_number")}}).fail(()=>{validator.removeClass("btn-success").addClass("btn-warning").text(WPInv.error);this.show_error(WPInv.vatValidationError,".getpaid-error-billingwpinv_vat_number")}).always(()=>{validator.prop("disabled",false);setTimeout(()=>{validator.removeClass("btn-success btn-danger btn-warning").addClass("btn-primary").text(validator.data("validate"))},3e3)})},init(){this.setup_saved_payment_tokens();this.attach_events();this.refresh_state();this.form.find(".getpaid-payment-form-element-billing_email span.d-none").closest(".col-12").addClass("d-none");this.form.find(".getpaid-gateway-description:not(:has(*))").remove();var address_toggle=this.form.find('[name ="same-shipping-address"]');if(address_toggle.length>0){this.handleAddressToggle(address_toggle)}$("body").trigger("getpaid_setup_payment_form",[this.form])}}};var setup_form=function(form){form.find(".getpaid-gateway-descriptions-div .form-horizontal .form-group").addClass("row");function filter_form_cart(selected_items){if(0==form.find(".getpaid-payment-form-items-cart").length){return}form.find(".getpaid-payment-form-items-cart-item.getpaid-selectable").each(function(){$(this).find(".getpaid-item-price-input").attr("name","");$(this).find(".getpaid-item-quantity-input").attr("name","");$(this).hide()});$(selected_items).each(function(index,item_id){if(item_id){var item=form.find(".getpaid-payment-form-items-cart-item.item-"+item_id);item.find(".getpaid-item-price-input").attr("name","getpaid-items["+item_id+"][price]");item.find(".getpaid-item-quantity-input").attr("name","getpaid-items["+item_id+"][quantity]");item.show()}})}if(form.find(".getpaid-payment-form-items-radio").length){var filter_totals=function(){var selected_item=form.find(".getpaid-payment-form-items-radio .form-check-input:checked").val();filter_form_cart([selected_item])};var radio_items=form.find(".getpaid-payment-form-items-radio .form-check-input");radio_items.on("change",filter_totals);if(0===radio_items.filter(":checked").length){radio_items.eq(0).prop("checked",true)}filter_totals()}if(form.find(".getpaid-payment-form-items-checkbox").length){var filter_totals=function(){var selected_items=form.find(".getpaid-payment-form-items-checkbox input:checked").map(function(){return $(this).val()}).get();filter_form_cart(selected_items)};var checkbox_items=form.find(".getpaid-payment-form-items-checkbox input");checkbox_items.on("change",filter_totals);if(0===checkbox_items.filter(":checked").length){checkbox_items.eq(0).prop("checked",true)}filter_totals()}if(form.find(".getpaid-payment-form-items-select").length){var filter_totals=function(){var selected_item=form.find(".getpaid-payment-form-items-select select").val();filter_form_cart([selected_item])};var select_box=form.find(".getpaid-payment-form-items-select select");select_box.on("change",filter_totals);if(!select_box.val()){select_box.find("option:first").prop("selected","selected")}filter_totals()}getpaid_form(form).init();form.on("submit",function(e){e.preventDefault();wpinvBlock(form);form.find(".getpaid-payment-form-errors, .getpaid-custom-payment-form-errors").html("").addClass("d-none");form.find(".is-invalid,.is-valid").removeClass("is-invalid is-valid");var unique_key=form.data("key");var data={submit:true,delay:false,data:form.serialize(),form:form,key:unique_key};if("no"==form.data("isFree")){$("body").trigger("getpaid_payment_form_before_submit",[data])}if(!data.submit){wpinvUnblock(form);return}var unblock=true;var submit=function(){var done=function(res){if("string"==typeof res){form.find(".getpaid-payment-form-errors").html(res).removeClass("d-none");return}if(res.success){if(!res.data.action||"redirect"==res.data.action){window.location.href=decodeURIComponent(res.data)}if("auto_submit_form"==res.data.action){form.parent().append('
'+res.data.form+"
");$(".getpaid-checkout-autosubmit-form form").submit()}if("event"==res.data.action){$("body").trigger(res.data.event,[res.data.data,form]);unblock=false}return}form.find(".getpaid-payment-form-errors").html(res.data).removeClass("d-none");form.find(".getpaid-payment-form-remove-on-error").remove();document.dispatchEvent(new Event("ayecode_reset_captcha"));if(res.invoice){localStorage.setItem("getpaid_last_invoice_"+form.find('input[name="form_id"]').val(),res.invoice);if(form.find('input[name="invoice_id"]').length==0){form.append('')}form.find('input[name="invoice_id"]').val(res.invoice)}};var fail=function(res){form.find(".getpaid-payment-form-errors").html(WPInv.connectionError).removeClass("d-none");form.find(".getpaid-payment-form-remove-on-error").remove()};var always=function(){if(unblock){wpinvUnblock(form)}};if(WPInv.recaptchaSettings&&WPInv.recaptchaSettings.enabled&&WPInv.recaptchaSettings.version=="v3"&&grecaptcha){return grecaptcha.ready(function(){grecaptcha.execute(WPInv.recaptchaSettings.sitekey,{action:"purchase"}).then(function(token){return $.post(WPInv.ajax_url,data.data+"&action=wpinv_payment_form&_ajax_nonce="+WPInv.formNonce+"&g-recaptcha-response="+token).done(done).fail(fail).always(always)})})}else{return $.post(WPInv.ajax_url,data.data+"&action=wpinv_payment_form&_ajax_nonce="+WPInv.formNonce).done(done).fail(fail).always(always)}};if(data.delay){var local_submit=function(){if(!data.submit){wpinvUnblock(form)}else{submit()}$("body").unbind("getpaid_payment_form_delayed_submit"+unique_key,local_submit)};$("body").bind("getpaid_payment_form_delayed_submit"+unique_key,local_submit);return}submit()})};$(".getpaid-payment-form").each(function(){setup_form($(this))});$(document).on("click",".getpaid-payment-button",function(e){e.preventDefault();$("#getpaid-payment-modal .modal-body-wrapper").html('
'+WPInv.loading+"
");if(window.bootstrap&&window.bootstrap.Modal){var paymentModal=new window.bootstrap.Modal(document.getElementById("getpaid-payment-modal"));paymentModal.show()}else{$("#getpaid-payment-modal").modal()}var data=$(this).data();data.action="wpinv_get_payment_form";data._ajax_nonce=WPInv.formNonce;data.current_url=window.location.href;$.get(WPInv.ajax_url,data,function(res){$("#getpaid-payment-modal .modal-body-wrapper").html(res);if(paymentModal){paymentModal.handleUpdate()}else{$("#getpaid-payment-modal").modal("handleUpdate")}$("#getpaid-payment-modal .getpaid-payment-form").each(function(){setup_form($(this))})}).fail(function(res){$("#getpaid-payment-modal .modal-body-wrapper").html(WPInv.connectionError);if(paymentModal){paymentModal.handleUpdate()}else{$("#getpaid-payment-modal").modal("handleUpdate")}})});$(document).on("click",'a[href^="#getpaid-form-"], a[href^="#getpaid-item-"]',function(e){var attr=$(this).attr("href");if(-1!=attr.indexOf("#getpaid-form-")){var data={form:attr.replace("#getpaid-form-","")}}else if(-1!=attr.indexOf("#getpaid-item-")){var data={item:attr.replace("#getpaid-item-","")}}else{return}e.preventDefault();$("#getpaid-payment-modal .modal-body-wrapper").html('
'+WPInv.loading+"
");if(window.bootstrap&&window.bootstrap.Modal){var paymentModal=new window.bootstrap.Modal(document.getElementById("getpaid-payment-modal"));paymentModal.show()}else{$("#getpaid-payment-modal").modal()}data.action="wpinv_get_payment_form";data._ajax_nonce=WPInv.formNonce;$.get(WPInv.ajax_url,data,function(res){$("#getpaid-payment-modal .modal-body-wrapper").html(res);if(paymentModal){paymentModal.handleUpdate()}else{$("#getpaid-payment-modal").modal("handleUpdate")}$("#getpaid-payment-modal .getpaid-payment-form").each(function(){setup_form($(this))})}).fail(function(res){$("#getpaid-payment-modal .modal-body-wrapper").html(WPInv.connectionError);if(paymentModal){paymentModal.handleUpdate()}else{$("#getpaid-payment-modal").modal("handleUpdate")}})});$(document).on("change",".getpaid-address-edit-form #wpinv-country",function(e){var state=$(this).closest(".getpaid-address-edit-form").find(".wpinv_state");if(state.length){wpinvBlock(state.parent());var data={action:"wpinv_get_aui_states_field",country:$(this).val(),state:state.val(),class:"wpinv_state",name:state.attr("name"),_ajax_nonce:WPInv.nonce};$.get(WPInv.ajax_url,data,res=>{if("object"==typeof res){state.parent().replaceWith(res.data.html)}}).always(()=>{wpinvUnblock(state.parent())})}});RegExp.getpaidquote=function(str){return str.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")};$(document).on("input",".getpaid-validate-minimum-amount",function(e){if(""===$(this).val()){return}var thousands=new RegExp(RegExp.getpaidquote(WPInv.thousands),"g");var decimals=new RegExp(RegExp.getpaidquote(WPInv.decimals),"g");var val=$(this).val();val=val.replace(thousands,"");val=val.replace(decimals,".");if(!isNaN(parseFloat(val))&&$(this).data("minimum-amount")&&parseFloat(val)
'+message+"
")}}function wpinvUnblock(el){var $el=jQuery(el);if(1==$el.data("GetPaidIsBlocked")){$el.data("GetPaidIsBlocked",0);if(!$el.data("GetPaidWasRelative")){$el.removeClass("position-relative")}$el.children(".getpaid-block-ui").remove()}} \ No newline at end of file +function wpinvBlock(e,t){t=void 0!==t&&""!==t?t:WPInv.loading;var a=jQuery(e);1!=a.data("GetPaidIsBlocked")&&(a.data("GetPaidIsBlocked",1),a.data("GetPaidWasRelative",a.hasClass("position-relative")),a.addClass("position-relative"),a.append('
'+t+"
"))}function wpinvUnblock(e){var t=jQuery(e);1==t.data("GetPaidIsBlocked")&&(t.data("GetPaidIsBlocked",0),t.data("GetPaidWasRelative")||t.removeClass("position-relative"),t.children(".getpaid-block-ui").remove())}jQuery((function(e){var t=[],a=!1;window.getpaid_form=function(i){return{fetched_initial_state:0,cached_states:{},form:i,show_error(t,a){i.find(".getpaid-payment-form-errors, .getpaid-custom-payment-form-errors").html("").addClass("d-none"),a&&i.find(a).length?(i.find(a).html(t).removeClass("d-none"),i.find(a).closest(".form-group").find(".form-control").addClass("is-invalid"),i.find(a).closest(".form-group").find(".getpaid-custom-payment-form-success").addClass("d-none")):(i.find(".getpaid-payment-form-errors").html(t).removeClass("d-none"),i.find(".getpaid-custom-payment-form-errors").each((function(){var t=e(this).closest(".form-group").find(".form-control");""!=t.val()&&t.addClass("is-valid")})))},hide_error(){i.find(".getpaid-payment-form-errors, .getpaid-custom-payment-form-errors").html("").addClass("d-none"),i.find(".is-invalid, .is-valid").removeClass("is-invalid is-valid")},cache_state(e,t){this.cached_states[e]=t},current_state_key(){return this.form.serialize()},is_current_state_cached(){return this.cached_states.hasOwnProperty(this.current_state_key())},switch_state(){this.hide_error();var t=this.cached_states[this.current_state_key()];if(!t)return this.fetch_state();var a=this.form.find(".getpaid-tax-template");if(t.totals)for(var n in t.totals)t.totals.hasOwnProperty(n)&&this.form.find(".getpaid-form-cart-totals-total-"+n).html(t.totals[n]);if(a.length&&(this.form.find(".getpaid-form-cart-totals-tax:not(.getpaid-tax-template)").remove(),!Array.isArray(t.taxes))){var s=a.prop("outerHTML");for(var r in t.taxes)if(t.taxes.hasOwnProperty(r)){var d=a.before(s).prev().removeClass("getpaid-tax-template d-none");d.find(".getpaid-payment-form-line-totals-label").html(r),d.find(".getpaid-payment-form-line-totals-value").html(t.taxes[r])}}if(Array.isArray(t.fees)?this.form.find(".getpaid-form-cart-totals-fees").addClass("d-none"):this.form.find(".getpaid-form-cart-totals-fees").removeClass("d-none"),Array.isArray(t.discounts)?this.form.find(".getpaid-form-cart-totals-discount").addClass("d-none"):this.form.find(".getpaid-form-cart-totals-discount").removeClass("d-none"),t.items)for(var o in t.items)t.items.hasOwnProperty(o)&&this.form.find(".getpaid-form-cart-item-subtotal-"+o).html(t.items[o]);if(t.selected_items)for(var o in t.selected_items)t.selected_items.hasOwnProperty(o)&&(this.form.find('input[name="getpaid-items['+o+'][price]"]').val(t.selected_items[o].price),this.form.find('input[name="getpaid-items['+o+'][quantity]"]').val(t.selected_items[o].quantity),this.form.find(".getpaid-items-"+o+"-view-price").html(t.selected_items[o].price_fmt),this.form.find(".getpaid-items-"+o+"-view-quantity").text(t.selected_items[o].quantity));if(t.texts)for(var p in t.texts)t.texts.hasOwnProperty(p)&&this.form.find(p).html(t.texts[p]);t.gateways&&this.process_gateways(t.gateways,t),t.invoice&&(0==this.form.find('input[name="invoice_id"]').length&&this.form.append(''),this.form.find('input[name="invoice_id"]').val(t.invoice)),t.js_data&&this.form.data("getpaid_js_data",t.js_data),i.find(".getpaid-custom-payment-form-errors.d-none").each((function(){var t=e(this).closest(".form-group").find(".form-control");""!=t.val()&&t.addClass("is-valid").closest(".form-group").find(".getpaid-custom-payment-form-success").removeClass("d-none")})),this.setup_saved_payment_tokens(),this.form.trigger("getpaid_payment_form_changed_state",[t])},refresh_state(){if(this.is_current_state_cached())return this.switch_state();this.fetch_state()},fetch_state(){wpinvBlock(this.form);var t=this.current_state_key(),a=this.fetched_initial_state?"0":localStorage.getItem("getpaid_last_invoice_"+this.form.find('input[name="form_id"]').val());return e.post(WPInv.ajax_url,t+"&action=wpinv_payment_form_refresh_prices&_ajax_nonce="+WPInv.formNonce+"&initial_state="+this.fetched_initial_state+"&maybe_use_invoice="+a).done((e=>{if(e.success)return this.fetched_initial_state=1,this.cache_state(t,e.data),this.switch_state();!1!==e.success?this.show_error(e):this.show_error(e.data.error,e.data.code)})).fail((()=>{this.show_error(WPInv.connectionError)})).always((()=>{wpinvUnblock(this.form)}))},update_state_field(t){if((t=e(t)).find(".wpinv_state").length){var a=t.find(".getpaid-address-field-wrapper__state");wpinvBlock(a);var i={action:"wpinv_get_payment_form_states_field",country:t.find(".wpinv_country").val(),form:this.form.find('input[name="form_id"]').val(),name:a.find(".wpinv_state").attr("name"),_ajax_nonce:WPInv.formNonce};e.get(WPInv.ajax_url,i,(e=>{"object"==typeof e&&a.replaceWith(e.data)})).always((()=>{wpinvUnblock(t.find(".getpaid-address-field-wrapper__state"))}))}},attach_events(){var n=this,s=function(e,t){t||(t=200);var a=!1,i=!0;return function(){if(a){i=!1;var n=this;setTimeout((function(){i||(e.bind(n).call(),i=!0)}),t)}else i=!0,e.bind(this).call(),a=!0,setTimeout((function(){a=!1}),t)}}((function(){n.refresh_state()}),500);this.form.on("change",".getpaid-refresh-on-change",s),this.form.on("input",".getpaid-payment-form-element-price_select :input:not(.getpaid-refresh-on-change)",s),this.form.on("change",".getpaid-payment-form-element-currency_select :input:not(.getpaid-refresh-on-change)",s),this.form.on("change",".getpaid-item-quantity-input",s),this.form.on("change",'[name="getpaid-payment-form-selected-item"]',s),this.form.on("change",".getpaid-item-mobile-quantity-input",(function(){let t=e(this);t.closest(".getpaid-payment-form-items-cart-item").find(".getpaid-item-quantity-input").val(t.val()).trigger("change")})),this.form.on("change",".getpaid-item-quantity-input",(function(){let t=e(this);t.closest(".getpaid-payment-form-items-cart-item").find(".getpaid-item-mobile-quantity-input").val(t.val())})),this.form.on("change",".getpaid-item-price-input",(function(){e(this).hasClass("is-invalid")||s()})),this.form.on("keypress",".getpaid-refresh-on-change, .getpaid-payment-form-element-price_select :input:not(.getpaid-refresh-on-change), .getpaid-item-quantity-input, .getpaid-item-price-input",(function(e){"13"==e.keyCode&&(e.preventDefault(),s())})),this.form.on("change",".getpaid-shipping-address-wrapper .wpinv_country",(()=>{this.update_state_field(".getpaid-shipping-address-wrapper")})),this.form.on("change",".getpaid-billing-address-wrapper .wpinv_country",(()=>{this.update_state_field(".getpaid-billing-address-wrapper"),this.form.find(".getpaid-billing-address-wrapper .wpinv_country").val()!=this.form.find(".getpaid-billing-address-wrapper .wpinv_country").data("ipCountry")?this.form.find(".getpaid-address-field-wrapper__address-confirm").removeClass("d-none"):this.form.find(".getpaid-address-field-wrapper__address-confirm").addClass("d-none"),s()})),this.form.on("change",".getpaid-billing-address-wrapper .wpinv_state, .getpaid-billing-address-wrapper .wpinv_vat_number",(()=>{s()})),this.form.on("click",".getpaid-vat-number-validate",(e=>{e.preventDefault(),this.validate_vat_number()})),this.form.on("click",'[name="confirm-address"]',(()=>{s()})),this.form.on("change",".getpaid-billing-address-wrapper .wpinv_vat_number",(function(){var t=e(this).parent().find(".getpaid-vat-number-validate");t.text(t.data("validate"))})),this.form.on("input",".getpaid-format-card-number",(function(){var t=e(this),a=t.val(),i=a.replace(/\D/g,"").replace(/(.{4})/g,"$1 ");a!=i&&t.val(i)})),this.form.on("change",'.getpaid-authorizenet-payment-type-selector input[type="radio"]',(function(){var t=e(this).val(),a=e(this).closest(".getpaid-gateway-description");"ach"===t?(a.find(".getpaid-authorizenet-cc-section").hide(),a.find(".getpaid-authorizenet-ach-section").show()):(a.find(".getpaid-authorizenet-cc-section").show(),a.find(".getpaid-authorizenet-ach-section").hide())})),this.form.on("input",".getpaid-ach-routing-number",(function(){var t=e(this),a=t.val().replace(/\D/g,"").substring(0,9);t.val()!==a&&t.val(a)})),this.form.on("input",".getpaid-ach-account-number",(function(){var t=e(this),a=t.val().replace(/\D/g,"").substring(0,17);t.val()!==a&&t.val(a)})),this.form.find(".getpaid-discount-field").length&&(this.form.find(".getpaid-discount-button").on("click",(function(e){e.preventDefault(),s()})),this.form.find(".getpaid-discount-field").on("keypress",(function(e){"13"==e.keyCode&&(e.preventDefault(),s())})),this.form.find(".getpaid-discount-field").on("change",(function(e){s()}))),this.form.on("change",".getpaid-gateway-radio input",(()=>{var e=this.form.find(".getpaid-gateway-radio input:checked").val();i.find(".getpaid-gateway-description").slideUp(),i.find(`.getpaid-description-${e}`).slideDown()})),this.form.find(".getpaid-file-upload-element").each((function(){let t,a=e(this),i=a.closest(".upload-file")||a.closest(".form-group"),n=i.find(".getpaid-uploaded-files"),s=parseInt(i.data("max")||1),r=[],d=function(a){if(!a)return;let d=i.find(".getpaid-progress-template").clone().removeClass("d-none getpaid-progress-template");n.append(d),d.find("a.close").on("click",(function(e){e.preventDefault();let i=r.indexOf(a);i>-1&&(r=r.splice(i,1)),d.fadeOut(300,(()=>{d.remove()}));try{t&&t.abort()}catch(e){}})),d.find(".getpaid-progress-file-name").text(a.name).attr("title",a.name),d.find(".progress-bar").attr("aria-valuemax",a.size);let o={"application/pdf":'',"application/zip":'',"application/x-gzip":'',"application/rar":'',"application/x-7z-compressed":'',"application/x-tar":'',audio:'',image:'',video:'',"application/msword":'',"application/vnd.ms-excel":'',"application/msword":'',"application/vnd.ms-word":'',"application/vnd.ms-powerpoint":''};if(a.type&&Object.keys(o).forEach((function(e){-1!==a.type.indexOf(e)&&d.find(".fa.fa-file").replaceWith(o[e])})),s&&r.length>=s)return void d.find(".getpaid-progress").html('");let p=a.name.match(/\.([^\.]+)$/)[1];if(i.find(".getpaid-files-input").data("extensions").indexOf(p.toString().toLowerCase())<0)return void d.find(".getpaid-progress").html('");let l=new FormData;l.append("file",a),l.append("action","wpinv_file_upload"),l.append("form_id",d.closest("form").find('input[name="form_id"]').val()),l.append("_ajax_nonce",WPInv.formNonce),l.append("field_name",i.data("name")),r.push(a),t=e.ajax({url:WPInv.ajax_url,type:"POST",contentType:!1,processData:!1,data:l,xhr:function(){let e=new window.XMLHttpRequest;return e.upload.addEventListener("progress",(function(e){if(e.lengthComputable){let t=Math.round(100*e.loaded/e.total)+"%";d.find(".progress-bar").attr("aria-valuenow",e.loaded).css("width",t).text(t)}}),!1),e},success:function(e){e.success?d.append(e.data):d.find(".getpaid-progress").html('")},error:function(e,t,a){d.find(".getpaid-progress").html('")}})},o=function(e){Array.prototype.forEach.apply(e,[d])};a.on("dragenter",(()=>{a.addClass("getpaid-trying-to-drop")})).on("dragover",(e=>{(e=e.originalEvent).stopPropagation(),e.preventDefault(),e.dataTransfer.dropEffect="copy"})).on("dragleave",(()=>{a.removeClass("getpaid-trying-to-drop")})).on("drop",(e=>{(e=e.originalEvent).stopPropagation(),e.preventDefault();let t=e.dataTransfer.files;t.length>0&&o(t)})),i.find(".getpaid-files-input").on("change",(function(e){let t=e.originalEvent.target.files;t&&(o(t),i.find(".getpaid-files-input").val(""))}))})),jQuery.fn.popover&&this.form.find(".gp-tooltip").length&&this.form.find(".gp-tooltip").popover({container:this.form[0],html:!0,content:function(){return e(this).closest(".getpaid-form-cart-item-name").find(".getpaid-item-desc").html()}}),jQuery.fn.flatpickr&&this.form.find(".getpaid-init-flatpickr").length&&this.form.find(".getpaid-init-flatpickr").each((function(){let e={},t=jQuery(this);if(t.data("disable_alt")&&t.data("disable_alt").length>0&&(e.disable=t.data("disable_alt")),t.data("disable_days_alt")&&t.data("disable_days_alt").length>0){e.disable=e.disable||[];let a=t.data("disable_days_alt");e.disable.push((function(e){return a.indexOf(e.getDay())>=0}))}jQuery(this).removeClass("flatpickr-input").flatpickr(e)})),WPInv.recaptchaSettings&&WPInv.recaptchaSettings.enabled&&"v2"==WPInv.recaptchaSettings.version&&function(e){if(a)e();else{t.push(e);var i=setInterval((function(){"undefined"!=typeof grecaptcha&&(clearInterval(i),a=!0,t.forEach((function(e){e()})))}),100)}}((()=>{var e=this.form.find(".getpaid-recaptcha-wrapper .g-recaptcha").attr("id");e&&grecaptcha.ready((()=>{grecaptcha.render(e,WPInv.recaptchaSettings.render_params)}))}))},process_gateways(t,a){this.form.data("initial_amt",a.initial_amt),this.form.data("currency",a.currency);var i=this.form.find(".getpaid-payment-form-submit"),n=i.data("free").replace(/%price%/gi,a.totals.raw_total),s=i.data("pay").replace(/%price%/gi,a.totals.raw_total);return i.prop("disabled",!1).css("cursor","pointer"),a.is_free?(i.val(n),this.form.find(".getpaid-gateways").slideUp(),void this.form.data("isFree","yes")):(this.form.data("isFree","no"),this.form.find(".getpaid-gateways").slideDown(),i.val(s),this.form.find(".getpaid-no-recurring-gateways, .getpaid-no-subscription-group-gateways, .getpaid-no-multiple-subscription-group-gateways, .getpaid-no-active-gateways").addClass("d-none"),this.form.find(".getpaid-select-gateway-title-div, .getpaid-available-gateways-div, .getpaid-gateway-descriptions-div").removeClass("d-none"),t.length<1?(this.form.find(".getpaid-select-gateway-title-div, .getpaid-available-gateways-div, .getpaid-gateway-descriptions-div").addClass("d-none"),i.prop("disabled",!0).css("cursor","not-allowed"),a.has_multiple_subscription_groups?void this.form.find(".getpaid-no-multiple-subscription-group-gateways").removeClass("d-none"):a.has_subscription_group?void this.form.find(".getpaid-no-subscription-group-gateways").removeClass("d-none"):a.has_recurring?void this.form.find(".getpaid-no-recurring-gateways").removeClass("d-none"):void this.form.find(".getpaid-no-active-gateways").removeClass("d-none")):(1==t.length?(this.form.find(".getpaid-select-gateway-title-div").addClass("d-none"),this.form.find(".getpaid-gateway-radio input").addClass("d-none")):this.form.find(".getpaid-gateway-radio input").removeClass("d-none"),this.form.find(".getpaid-gateway").addClass("d-none"),e.each(t,((e,t)=>{this.form.find(`.getpaid-gateway-${t}`).removeClass("d-none")})),0===this.form.find(".getpaid-gateway:visible input:checked").length&&this.form.find(".getpaid-gateway:visible .getpaid-gateway-radio input").eq(0).prop("checked",!0),void(0===this.form.find(".getpaid-gateway-description:visible").length&&this.form.find(".getpaid-gateway-radio input:checked").trigger("change"))))},setup_saved_payment_tokens(){var t=this.form.data("currency");this.form.find(".getpaid-saved-payment-methods").each((function(){var a=e(this);a.show(),e("input",a).on("change",(function(){e(this).closest("li").hasClass("getpaid-new-payment-method")?a.closest(".getpaid-gateway-description").find(".getpaid-new-payment-method-form").slideDown():a.closest(".getpaid-gateway-description").find(".getpaid-new-payment-method-form").slideUp()})),a.find("input").each((function(){"none"!=e(this).data("currency")&&t!=e(this).data("currency")?(e(this).closest("li").addClass("d-none"),e(this).prop("checked",!1)):e(this).closest("li").removeClass("d-none")})),0===e("li:not(.d-none) input",a).filter(":checked").length&&e("li:not(.d-none) input",a).eq(0).prop("checked",!0),0===e("li:not(.d-none) input",a).filter(":checked").length&&e("input",a).last().prop("checked",!0),2>e("li:not(.d-none) input",a).length&&a.hide(),e("input",a).filter(":checked").trigger("change")}))},handleAddressToggle(t){var a=t.closest(".getpaid-payment-form-element-address");a.find(".getpaid-billing-address-title, .getpaid-shipping-address-title, .getpaid-shipping-address-wrapper").addClass("d-none"),t.on("change",(function(){e(this).is(":checked")?(a.find(".getpaid-billing-address-title, .getpaid-shipping-address-title, .getpaid-shipping-address-wrapper").addClass("d-none"),a.find(".getpaid-shipping-billing-address-title").removeClass("d-none")):(a.find(".getpaid-billing-address-title, .getpaid-shipping-address-title, .getpaid-shipping-address-wrapper").removeClass("d-none"),a.find(".getpaid-shipping-billing-address-title").addClass("d-none"))}))},validate_vat_number(){var t=this.form.find(".wpinv_vat_number"),a=this.form.find(".wpinv_country"),i=t.parent().find(".getpaid-vat-number-validate");if(t.length&&a.length){var n=t.val(),s=a.val();n&&s?(i.prop("disabled",!0).text(WPInv.validating),e.post(WPInv.ajax_url,{action:"wpinv_validate_vat_number",vat_number:n,country:s,_ajax_nonce:WPInv.formNonce}).done((e=>{e.success?(i.removeClass("btn-primary").addClass("btn-success").text(WPInv.valid),t.removeClass("is-invalid").addClass("is-valid"),this.hide_error()):(i.removeClass("btn-success").addClass("btn-danger").text(WPInv.invalid),t.removeClass("is-valid").addClass("is-invalid"),this.show_error(e.data.message,".getpaid-error-billingwpinv_vat_number"))})).fail((()=>{i.removeClass("btn-success").addClass("btn-warning").text(WPInv.error),this.show_error(WPInv.vatValidationError,".getpaid-error-billingwpinv_vat_number")})).always((()=>{i.prop("disabled",!1),setTimeout((()=>{i.removeClass("btn-success btn-danger btn-warning").addClass("btn-primary").text(i.data("validate"))}),3e3)}))):this.show_error(WPInv.vatFieldsRequired,".getpaid-error-billingwpinv_vat_number")}},init(){this.setup_saved_payment_tokens(),this.attach_events(),this.refresh_state(),this.form.find(".getpaid-payment-form-element-billing_email span.d-none").closest(".col-12").addClass("d-none"),this.form.find(".getpaid-gateway-description:not(:has(*))").remove();var t=this.form.find('[name ="same-shipping-address"]');t.length>0&&this.handleAddressToggle(t),e("body").trigger("getpaid_setup_payment_form",[this.form])}}};var i=function(t){function a(a){0!=t.find(".getpaid-payment-form-items-cart").length&&(t.find(".getpaid-payment-form-items-cart-item.getpaid-selectable").each((function(){e(this).find(".getpaid-item-price-input").attr("name",""),e(this).find(".getpaid-item-quantity-input").attr("name",""),e(this).hide()})),e(a).each((function(e,a){if(a){var i=t.find(".getpaid-payment-form-items-cart-item.item-"+a);i.find(".getpaid-item-price-input").attr("name","getpaid-items["+a+"][price]"),i.find(".getpaid-item-quantity-input").attr("name","getpaid-items["+a+"][quantity]"),i.show()}})))}if(t.find(".getpaid-gateway-descriptions-div .form-horizontal .form-group").addClass("row"),t.find(".getpaid-payment-form-items-radio").length){var i=function(){a([t.find(".getpaid-payment-form-items-radio .form-check-input:checked").val()])},n=t.find(".getpaid-payment-form-items-radio .form-check-input");n.on("change",i),0===n.filter(":checked").length&&n.eq(0).prop("checked",!0),i()}if(t.find(".getpaid-payment-form-items-checkbox").length){i=function(){a(t.find(".getpaid-payment-form-items-checkbox input:checked").map((function(){return e(this).val()})).get())};var s=t.find(".getpaid-payment-form-items-checkbox input");s.on("change",i),0===s.filter(":checked").length&&s.eq(0).prop("checked",!0),i()}if(t.find(".getpaid-payment-form-items-select").length){i=function(){a([t.find(".getpaid-payment-form-items-select select").val()])};var r=t.find(".getpaid-payment-form-items-select select");r.on("change",i),r.val()||r.find("option:first").prop("selected","selected"),i()}getpaid_form(t).init(),t.on("submit",(function(a){a.preventDefault(),wpinvBlock(t),t.find(".getpaid-payment-form-errors, .getpaid-custom-payment-form-errors").html("").addClass("d-none"),t.find(".is-invalid,.is-valid").removeClass("is-invalid is-valid");var i=t.data("key"),n={submit:!0,delay:!1,data:t.serialize(),form:t,key:i};if("no"==t.data("isFree")&&e("body").trigger("getpaid_payment_form_before_submit",[n]),n.submit){var s=!0,r=function(){var a=function(a){if("string"!=typeof a){if(a.success)return a.data.action&&"redirect"!=a.data.action||(window.location.href=decodeURIComponent(a.data)),"auto_submit_form"==a.data.action&&(t.parent().append('
'+a.data.form+"
"),e(".getpaid-checkout-autosubmit-form form").submit()),void("event"==a.data.action&&(e("body").trigger(a.data.event,[a.data.data,t]),s=!1));t.find(".getpaid-payment-form-errors").html(a.data).removeClass("d-none"),t.find(".getpaid-payment-form-remove-on-error").remove(),document.dispatchEvent(new Event("ayecode_reset_captcha")),a.invoice&&(localStorage.setItem("getpaid_last_invoice_"+t.find('input[name="form_id"]').val(),a.invoice),0==t.find('input[name="invoice_id"]').length&&t.append(''),t.find('input[name="invoice_id"]').val(a.invoice))}else t.find(".getpaid-payment-form-errors").html(a).removeClass("d-none")},i=function(e){t.find(".getpaid-payment-form-errors").html(WPInv.connectionError).removeClass("d-none"),t.find(".getpaid-payment-form-remove-on-error").remove()},r=function(){s&&wpinvUnblock(t)};return WPInv.recaptchaSettings&&WPInv.recaptchaSettings.enabled&&"v3"==WPInv.recaptchaSettings.version&&grecaptcha?grecaptcha.ready((function(){grecaptcha.execute(WPInv.recaptchaSettings.sitekey,{action:"purchase"}).then((function(t){return e.post(WPInv.ajax_url,n.data+"&action=wpinv_payment_form&_ajax_nonce="+WPInv.formNonce+"&g-recaptcha-response="+t).done(a).fail(i).always(r)}))})):e.post(WPInv.ajax_url,n.data+"&action=wpinv_payment_form&_ajax_nonce="+WPInv.formNonce).done(a).fail(i).always(r)};if(n.delay){var d=function(){n.submit?r():wpinvUnblock(t),e("body").unbind("getpaid_payment_form_delayed_submit"+i,d)};e("body").bind("getpaid_payment_form_delayed_submit"+i,d)}else r()}else wpinvUnblock(t)}))};e(".getpaid-payment-form").each((function(){i(e(this))})),e(document).on("click",".getpaid-payment-button",(function(t){if(t.preventDefault(),e("#getpaid-payment-modal .modal-body-wrapper").html('
'+WPInv.loading+"
"),window.bootstrap&&window.bootstrap.Modal){var a=new window.bootstrap.Modal(document.getElementById("getpaid-payment-modal"));a.show()}else e("#getpaid-payment-modal").modal();var n=e(this).data();n.action="wpinv_get_payment_form",n._ajax_nonce=WPInv.formNonce,n.current_url=window.location.href,e.get(WPInv.ajax_url,n,(function(t){e("#getpaid-payment-modal .modal-body-wrapper").html(t),a?a.handleUpdate():e("#getpaid-payment-modal").modal("handleUpdate"),e("#getpaid-payment-modal .getpaid-payment-form").each((function(){i(e(this))}))})).fail((function(t){e("#getpaid-payment-modal .modal-body-wrapper").html(WPInv.connectionError),a?a.handleUpdate():e("#getpaid-payment-modal").modal("handleUpdate")}))})),e(document).on("click",'a[href^="#getpaid-form-"], a[href^="#getpaid-item-"]',(function(t){var a=e(this).attr("href");if(-1!=a.indexOf("#getpaid-form-"))var n={form:a.replace("#getpaid-form-","")};else{if(-1==a.indexOf("#getpaid-item-"))return;n={item:a.replace("#getpaid-item-","")}}if(t.preventDefault(),e("#getpaid-payment-modal .modal-body-wrapper").html('
'+WPInv.loading+"
"),window.bootstrap&&window.bootstrap.Modal){var s=new window.bootstrap.Modal(document.getElementById("getpaid-payment-modal"));s.show()}else e("#getpaid-payment-modal").modal();n.action="wpinv_get_payment_form",n._ajax_nonce=WPInv.formNonce,e.get(WPInv.ajax_url,n,(function(t){e("#getpaid-payment-modal .modal-body-wrapper").html(t),s?s.handleUpdate():e("#getpaid-payment-modal").modal("handleUpdate"),e("#getpaid-payment-modal .getpaid-payment-form").each((function(){i(e(this))}))})).fail((function(t){e("#getpaid-payment-modal .modal-body-wrapper").html(WPInv.connectionError),s?s.handleUpdate():e("#getpaid-payment-modal").modal("handleUpdate")}))})),e(document).on("change",".getpaid-address-edit-form #wpinv-country",(function(t){var a=e(this).closest(".getpaid-address-edit-form").find(".wpinv_state");if(a.length){wpinvBlock(a.parent());var i={action:"wpinv_get_aui_states_field",country:e(this).val(),state:a.val(),class:"wpinv_state",name:a.attr("name"),_ajax_nonce:WPInv.nonce};e.get(WPInv.ajax_url,i,(e=>{"object"==typeof e&&a.parent().replaceWith(e.data.html)})).always((()=>{wpinvUnblock(a.parent())}))}})),RegExp.getpaidquote=function(e){return e.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")},e(document).on("input",".getpaid-validate-minimum-amount",(function(t){if(""!==e(this).val()){var a=new RegExp(RegExp.getpaidquote(WPInv.thousands),"g"),i=new RegExp(RegExp.getpaidquote(WPInv.decimals),"g"),n=e(this).val();n=(n=n.replace(a,"")).replace(i,"."),!isNaN(parseFloat(n))&&e(this).data("minimum-amount")&&parseFloat(n)get_log_months(); foreach ( $months as $month ) { - $selected = ( isset( $_GET['m'] ) && $_GET['m'] == $month->month ) ? ' selected="selected"' : ''; - echo ''; + $selected = ( isset( $_GET['m'] ) && $_GET['m'] == $month->year_month ) ? ' selected="selected"' : ''; + echo ''; } ?> @@ -193,14 +193,14 @@ private function get_log_months() { global $wpdb; $table_name = $wpdb->prefix . 'getpaid_anonymization_logs'; - $months = $wpdb->get_results( - "SELECT DISTINCT YEAR(timestamp) AS year, MONTH(timestamp) AS month, - DATE_FORMAT(timestamp, '%M') AS month_name, - DATE_FORMAT(timestamp, '%Y%m') AS month + return $wpdb->get_results( + "SELECT DISTINCT + YEAR(timestamp) AS year, + MONTH(timestamp) AS month_num, + DATE_FORMAT(timestamp, '%M') AS month_name, + DATE_FORMAT(timestamp, '%Y%m') AS year_month FROM $table_name - ORDER BY year DESC, month DESC" + ORDER BY year DESC, month_num DESC" ); - - return $months; } } \ No newline at end of file diff --git a/includes/class-wpinv-data-retention.php b/includes/class-wpinv-data-retention.php index f6326ee5..a365787f 100644 --- a/includes/class-wpinv-data-retention.php +++ b/includes/class-wpinv-data-retention.php @@ -17,647 +17,734 @@ */ class WPInv_Data_Retention { - /** - * Error message. - * - * @var string - */ - private $error_message; - - /** - * Flag to control whether user deletion should be handled. - * - * @var bool - */ - private $handle_user_deletion = true; - - /** - * Class constructor. - */ - public function __construct() { - add_filter( 'wpinv_settings_misc', array( $this, 'add_data_retention_settings' ) ); - - add_action( 'wpmu_delete_user', array( $this, 'maybe_handle_user_deletion' ), 1 ); - add_action( 'delete_user', array( $this, 'maybe_handle_user_deletion' ), 1 ); - add_filter( 'wp_privacy_personal_data_erasure_request', array( $this, 'handle_erasure_request' ), 10, 2 ); - - add_action( 'getpaid_daily_maintenance', array( $this, 'perform_data_retention_cleanup' ) ); - } - - /** - * Adds data retention settings to the misc settings page. - * - * @param array $misc_settings Existing misc settings. - * @return array Updated misc settings. - */ - public function add_data_retention_settings( $misc_settings ) { - $misc_settings['data_retention'] = array( - 'id' => 'data_retention', - 'name' => '

' . __( 'Data Retention', 'invoicing' ) . '

', - 'type' => 'header', - ); - - $misc_settings['data_retention_method'] = array( - 'id' => 'data_retention_method', - 'name' => __( 'Data Handling', 'invoicing' ), - 'desc' => __( 'Choose how to handle user data when deletion is required.', 'invoicing' ), - 'type' => 'select', - 'options' => array( - 'anonymize' => __( 'Anonymize data', 'invoicing' ), - 'delete' => __( 'Delete data without anonymization', 'invoicing' ), - ), - 'std' => 'anonymize', - 'tooltip' => __( 'Anonymization replaces personal data with non-identifiable information. Direct deletion removes all data permanently.', 'invoicing' ), - ); - - $misc_settings['data_retention_period'] = array( - 'id' => 'data_retention_period', - 'name' => __( 'Retention Period', 'invoicing' ), - 'desc' => __( 'Specify how long to retain customer data after processing.', 'invoicing' ), - 'type' => 'select', - 'options' => array( - 'never' => __( 'Never delete (retain indefinitely)', 'invoicing' ), - '30' => __( '30 days', 'invoicing' ), - '90' => __( '90 days', 'invoicing' ), - '180' => __( '6 months', 'invoicing' ), - '365' => __( '1 year', 'invoicing' ), - '730' => __( '2 years', 'invoicing' ), - '1825' => __( '5 years', 'invoicing' ), - '3650' => __( '10 years', 'invoicing' ), - ), - 'std' => '3650', - 'tooltip' => __( 'Choose how long to keep processed customer data before final action. This helps balance data minimization with business needs.', 'invoicing' ), - ); - - return $misc_settings; - } - - /** - * Conditionally handles user deletion based on the flag. - * - * @param int $user_id The ID of the user being deleted. - */ - public function maybe_handle_user_deletion( $user_id ) { - if ( ! $this->handle_user_deletion ) { - return; - } - - if ( current_user_can( 'manage_options' ) ) { - $this->handle_admin_user_deletion( $user_id ); - } else { - $this->handle_self_account_deletion( $user_id ); - } - } - - /** - * Handles admin-initiated user deletion process. - * - * @since 2.8.22 - * @param int $user_id The ID of the user being deleted. - */ - public function handle_admin_user_deletion( $user_id ) { - if ( $this->has_active_subscriptions( $user_id ) ) { - $this->prevent_user_deletion( $user_id, 'active_subscriptions' ); - return; - } - - if ( $this->has_paid_invoices( $user_id ) ) { - $retention_method = wpinv_get_option( 'data_retention_method', 'anonymize' ); - if ( 'anonymize' === $retention_method ) { - $this->anonymize_user_data( $user_id ); - $this->prevent_user_deletion( $user_id, 'paid_invoices' ); - } else { - $this->delete_user_data( $user_id ); - } - } - } - - /** - * Handles user account self-deletion. - * - * @since 2.8.22 - * @param int $user_id The ID of the user being deleted. - */ - public function handle_self_account_deletion( $user_id ) { - $this->cancel_active_subscriptions( $user_id ); - - if ( $this->has_paid_invoices( $user_id ) ) { - $retention_method = wpinv_get_option( 'data_retention_method', 'anonymize' ); - - if ( 'anonymize' === $retention_method ) { - $user = get_userdata( $user_id ); - - $this->anonymize_user_data( $user_id ); - - $message = apply_filters( 'uwp_get_account_deletion_message', '', $user ); - do_action( 'uwp_send_account_deletion_emails', $user, $message ); - - $this->end_user_session(); - } - } - } - - /** - * Checks if user has active subscriptions. - * - * @since 2.8.22 - * @param int $user_id The ID of the user being checked. - * @return bool True if user has active subscriptions, false otherwise. - */ - private function has_active_subscriptions( $user_id ) { - $subscriptions = getpaid_get_subscriptions( - array( - 'customer_in' => array( (int) $user_id ), - 'status' => 'active', - ) - ); - - return ! empty( $subscriptions ); - } - - /** - * Cancels all active subscriptions for a user. - * - * @since 2.8.22 - * @param int $user_id The ID of the user. - */ - private function cancel_active_subscriptions( $user_id ) { - $subscriptions = getpaid_get_subscriptions( - array( - 'customer_in' => array( (int) $user_id ), - 'status' => 'active', - ) - ); - - foreach ( $subscriptions as $subscription ) { - $subscription->cancel(); - } - } - - /** - * Checks if user has paid invoices. - * - * @since 2.8.22 - * @param int $user_id The ID of the user being checked. - * @return bool True if user has paid invoices, false otherwise. - */ - private function has_paid_invoices( $user_id ) { - $invoices = wpinv_get_invoices( - array( - 'user' => (int) $user_id, - 'status' => 'publish', - ) - ); - - return ! empty( $invoices->total ); - } - - /** - * Prevents user deletion by setting an error message and stopping execution. - * - * @since 2.8.22 - * @param int $user_id The ID of the user being deleted. - * @param string $reason The reason for preventing deletion. - */ - private function prevent_user_deletion( $user_id, $reason ) { - $user = get_userdata( $user_id ); - - if ( 'active_subscriptions' === $reason ) { - $this->error_message = sprintf( - /* translators: %s: user login */ - esc_html__( 'User deletion for %s has been halted. All active subscriptions should be cancelled first.', 'invoicing' ), - $user->user_login - ); - } else { - $this->error_message = sprintf( - /* translators: %s: user login */ - esc_html__( 'User deletion for %s has been halted due to paid invoices. Data will be anonymized instead.', 'invoicing' ), - $user->user_login - ); - } - - wp_die( $this->error_message, esc_html__( 'User Deletion Halted', 'invoicing' ), array( 'response' => 403 ) ); - } - - /** - * Anonymizes user data. - * - * @since 2.8.22 - * @param int $user_id The ID of the user to anonymize. - * @return bool True on success, false on failure. - */ - private function anonymize_user_data( $user_id ) { - global $wpdb; - - $user = get_userdata( $user_id ); - if ( ! $user ) { - return false; - } - - $table_name = $wpdb->prefix . 'getpaid_customers'; - $deletion_date = gmdate( 'Y-m-d', strtotime( '+10 years' ) ); - $hashed_email = $this->hash_email( $user->user_email ); - - $updated = $wpdb->update( - $table_name, - array( - 'is_anonymized' => 1, - 'deletion_date' => $deletion_date, - 'email' => $hashed_email, - 'email_cc' => $hashed_email, - 'phone' => '', - ), - array( 'user_id' => (int) $user->ID ) - ); - - if ( false === $updated ) { - return false; - } - - wp_update_user( - array( - 'ID' => (int) $user->ID, - 'user_email' => $hashed_email, - ) - ); - - /** - * Fires when anonymizing user meta fields. - * - * @since 2.8.22 - * @param int $user_id The ID of the user being anonymized. - */ - do_action( 'wpinv_anonymize_user_meta_data', $user->ID ); - - $user_meta_data = array( - 'nickname', + /** + * Error message. + * + * @var string + */ + private $error_message; + + /** + * Flag to control whether user deletion should be handled. + * + * @var bool + */ + private $handle_user_deletion = true; + + /** + * Class constructor. + */ + public function __construct() { + add_filter( 'wpinv_settings_misc', array( $this, 'add_data_retention_settings' ) ); + + add_action( 'wpmu_delete_user', array( $this, 'maybe_handle_user_deletion' ), 1 ); + add_action( 'delete_user', array( $this, 'maybe_handle_user_deletion' ), 1 ); + + // Register GDPR personal data eraser. + add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_eraser' ) ); + + add_action( 'getpaid_daily_maintenance', array( $this, 'perform_data_retention_cleanup' ) ); + } + + /** + * Adds data retention settings to the misc settings page. + * + * @param array $misc_settings Existing misc settings. + * @return array Updated misc settings. + */ + public function add_data_retention_settings( $misc_settings ) { + $misc_settings['data_retention'] = array( + 'id' => 'data_retention', + 'name' => '

' . __( 'Data Retention', 'invoicing' ) . '

', + 'type' => 'header', + ); + + $misc_settings['data_retention_method'] = array( + 'id' => 'data_retention_method', + 'name' => __( 'Data Handling', 'invoicing' ), + 'desc' => __( 'Choose how to handle user data when deletion is required.', 'invoicing' ), + 'type' => 'select', + 'options' => array( + 'anonymize' => __( 'Anonymize data', 'invoicing' ), + 'delete' => __( 'Delete data without anonymization', 'invoicing' ), + ), + 'std' => 'anonymize', + 'tooltip' => __( 'Anonymization replaces personal data with non-identifiable information. Direct deletion removes all data permanently.', 'invoicing' ), + ); + + $misc_settings['data_retention_period'] = array( + 'id' => 'data_retention_period', + 'name' => __( 'Retention Period', 'invoicing' ), + 'desc' => __( 'Specify how long to retain customer data after processing.', 'invoicing' ), + 'type' => 'select', + 'options' => array( + 'never' => __( 'Never delete (retain indefinitely)', 'invoicing' ), + '30' => __( '30 days', 'invoicing' ), + '90' => __( '90 days', 'invoicing' ), + '180' => __( '6 months', 'invoicing' ), + '365' => __( '1 year', 'invoicing' ), + '730' => __( '2 years', 'invoicing' ), + '1825' => __( '5 years', 'invoicing' ), + '3650' => __( '10 years', 'invoicing' ), + ), + 'std' => '3650', + 'tooltip' => __( 'Choose how long to keep processed customer data before final action. This helps balance data minimization with business needs.', 'invoicing' ), + ); + + return $misc_settings; + } + + /** + * Conditionally handles user deletion based on the flag. + * + * @param int $user_id The ID of the user being deleted. + */ + public function maybe_handle_user_deletion( $user_id ) { + if ( ! $this->handle_user_deletion ) { + return; + } + + if ( current_user_can( 'manage_options' ) ) { + $this->handle_admin_user_deletion( $user_id ); + } else { + $this->handle_self_account_deletion( $user_id ); + } + } + + /** + * Handles admin-initiated user deletion process. + * + * @since 2.8.22 + * @param int $user_id The ID of the user being deleted. + */ + public function handle_admin_user_deletion( $user_id ) { + if ( $this->has_active_subscriptions( $user_id ) ) { + $this->prevent_user_deletion( $user_id, 'active_subscriptions' ); + return; + } + + if ( $this->has_paid_invoices( $user_id ) ) { + $retention_method = wpinv_get_option( 'data_retention_method', 'anonymize' ); + if ( 'anonymize' === $retention_method ) { + $this->anonymize_user_data( $user_id ); + $this->prevent_user_deletion( $user_id, 'paid_invoices' ); + } else { + $this->delete_user_data( $user_id ); + } + } + } + + /** + * Handles user account self-deletion. + * + * @since 2.8.22 + * @param int $user_id The ID of the user being deleted. + */ + public function handle_self_account_deletion( $user_id ) { + $this->cancel_active_subscriptions( $user_id ); + + if ( $this->has_paid_invoices( $user_id ) ) { + $retention_method = wpinv_get_option( 'data_retention_method', 'anonymize' ); + + if ( 'anonymize' === $retention_method ) { + $user = get_userdata( $user_id ); + + $this->anonymize_user_data( $user_id ); + + $message = apply_filters( 'uwp_get_account_deletion_message', '', $user ); + do_action( 'uwp_send_account_deletion_emails', $user, $message ); + + $this->end_user_session(); + } + } + } + + /** + * Checks if user has active subscriptions. + * + * @since 2.8.22 + * @param int $user_id The ID of the user being checked. + * @return bool True if user has active subscriptions, false otherwise. + */ + private function has_active_subscriptions( $user_id ) { + $subscriptions = getpaid_get_subscriptions( + array( + 'customer_in' => array( (int) $user_id ), + 'status' => 'active', + ) + ); + + return ! empty( $subscriptions ); + } + + /** + * Cancels all active subscriptions for a user. + * + * @since 2.8.22 + * @param int $user_id The ID of the user. + */ + private function cancel_active_subscriptions( $user_id ) { + $subscriptions = getpaid_get_subscriptions( + array( + 'customer_in' => array( (int) $user_id ), + 'status' => 'active', + ) + ); + + foreach ( $subscriptions as $subscription ) { + $subscription->cancel(); + } + } + + /** + * Checks if user has paid invoices. + * + * @since 2.8.22 + * @param int $user_id The ID of the user being checked. + * @return bool True if user has paid invoices, false otherwise. + */ + private function has_paid_invoices( $user_id ) { + $invoices = wpinv_get_invoices( + array( + 'user' => (int) $user_id, + 'status' => 'publish', + ) + ); + + return ! empty( $invoices->total ); + } + + /** + * Prevents user deletion by setting an error message and stopping execution. + * + * @since 2.8.22 + * @param int $user_id The ID of the user being deleted. + * @param string $reason The reason for preventing deletion. + */ + private function prevent_user_deletion( $user_id, $reason ) { + $user = get_userdata( $user_id ); + + if ( 'active_subscriptions' === $reason ) { + $this->error_message = sprintf( + /* translators: %s: user login */ + esc_html__( 'User deletion for %s has been halted. All active subscriptions should be cancelled first.', 'invoicing' ), + $user->user_login + ); + } else { + $this->error_message = sprintf( + /* translators: %s: user login */ + esc_html__( 'User deletion for %s has been halted due to paid invoices. Data will be anonymized instead.', 'invoicing' ), + $user->user_login + ); + } + + wp_die( + $this->error_message, + esc_html__( 'User Deletion Halted', 'invoicing' ), + array( + 'response' => 403, + 'back_link' => true, + ) + ); + } + + /** + * Anonymizes user data. + * + * @since 2.8.22 + * @param int $user_id The ID of the user to anonymize. + * @return bool True on success, false on failure. + */ + private function anonymize_user_data( $user_id ) { + global $wpdb; + + $user = get_userdata( $user_id ); + if ( ! $user ) { + return false; + } + + $table_name = $wpdb->prefix . 'getpaid_customers'; + $retention_period = wpinv_get_option( 'data_retention_period', '3650' ); + $hashed_email = $this->hash_email( $user->user_email ); + + // Use the configured retention period for the deletion date. + $update_data = array( + 'is_anonymized' => 1, + 'email' => $hashed_email, + 'email_cc' => '', + 'first_name' => __( 'Anonymized', 'invoicing' ), + 'last_name' => '', + 'address' => '', + 'city' => '', + 'state' => '', + 'zip' => '', + 'phone' => '', + 'company' => '', + 'company_id' => '', + 'vat_number' => '', + ); + + if ( 'never' !== $retention_period ) { + $update_data['deletion_date'] = gmdate( 'Y-m-d', strtotime( "+{$retention_period} days" ) ); + } + + $updated = $wpdb->update( + $table_name, + $update_data, + array( 'user_id' => (int) $user->ID ) + ); + + if ( false === $updated ) { + return false; + } + + $anonymized_name = __( 'Anonymized User', 'invoicing' ); + $anonymized_login = 'anonymized_' . $user->ID; + + // Anonymize user_login directly since wp_update_user() does not allow changing it. + $wpdb->update( + $wpdb->users, + array( 'user_login' => $anonymized_login ), + array( 'ID' => (int) $user->ID ) + ); + + // Clear caches after direct DB update. + clean_user_cache( $user->ID ); + + wp_update_user( + array( + 'ID' => (int) $user->ID, + 'user_email' => $hashed_email, + 'display_name' => $anonymized_name, + 'first_name' => '', + 'last_name' => '', + 'nickname' => $anonymized_name, + 'user_nicename' => sanitize_title( $anonymized_login ), + 'user_url' => '', + 'description' => '', + ) + ); + + /** + * Fires when anonymizing user meta fields. + * + * @since 2.8.22 + * @param int $user_id The ID of the user being anonymized. + */ + do_action( 'wpinv_anonymize_user_meta_data', $user->ID ); + + $user_meta_data = array( + 'nickname', 'description', 'rich_editing', 'syntax_highlighting', 'comment_shortcuts', - 'admin_color', + 'admin_color', 'use_ssl', 'show_admin_bar_front', 'locale', - 'wp_capabilities', - 'wp_user_level', 'dismissed_wp_pointers', 'show_welcome_panel', - ); - - /** - * Filters the user meta fields to be anonymized. - * - * @since 2.8.22 - * @param array $user_meta_data The meta fields to be anonymized. - * @param int $user_id The ID of the user being anonymized. - */ - $user_meta_data = apply_filters( 'wpinv_user_meta_data_to_anonymize', $user_meta_data, $user->ID ); - - foreach ( $user_meta_data as $meta_key ) { - delete_user_meta( $user->ID, $meta_key ); - } - - return $this->ensure_invoice_anonymization( $user->ID, 'anonymize' ); - } - - /** - * Deletes user data without anonymization. - * - * @param int $user_id The ID of the user to delete. - * @return bool True on success, false on failure. - */ - private function delete_user_data( $user_id ) { - // Delete associated invoices. - $this->ensure_invoice_anonymization( $user_id, 'delete' ); - - // Delete the user. - if ( is_multisite() ) { - wpmu_delete_user( $user_id ); - } else { - wp_delete_user( $user_id ); - } - - /** - * Fires after deleting user data without anonymization. - * - * @since 2.8.22 - * @param int $user_id The ID of the user being deleted. - */ - do_action( 'wpinv_delete_user_data', $user_id ); - - return true; - } - - /** - * Ensures invoice data remains anonymized. - * - * @since 2.8.22 - * @param int $user_id The ID of the user whose invoices should be checked. - * @param string $action The action to perform (anonymize or delete). - * @return bool True on success, false on failure. - */ - public function ensure_invoice_anonymization( $user_id, $action = 'anonymize' ) { - $invoices = wpinv_get_invoices( array( 'user' => $user_id ) ); - - /** - * Filters the invoice meta fields to be anonymized. - * - * @since 2.8.22 - * @param array $inv_meta_data The meta fields to be anonymized. - * @param int $user_id The ID of the user being processed. - */ - $inv_meta_data = apply_filters( 'wpinv_invoice_meta_data_to_anonymize', array(), $user_id ); - - foreach ( $invoices->invoices as $invoice ) { - foreach ( $inv_meta_data as $meta_key ) { - delete_post_meta( $invoice->get_id(), $meta_key ); - } - - if ( 'anonymize' === $action ) { - $hashed_inv_email = $this->hash_email( $invoice->get_email() ); - $hashed_inv_email_cc = $this->hash_email( $invoice->get_email_cc() ); - - $invoice->set_email( $hashed_inv_email ); - $invoice->set_email_cc( $hashed_inv_email_cc ); - $invoice->set_phone( '' ); - $invoice->set_ip( $this->anonymize_data( $invoice->get_ip() ) ); - $invoice->set_is_anonymized( 1 ); - - /** - * Fires when anonymizing additional invoice data. - * - * @since 2.8.22 - * @param WPInv_Invoice $invoice The invoice being anonymized. - * @param string $action The action being performed (anonymize or delete). - */ - do_action( 'wpinv_anonymize_invoice_data', $invoice, $action ); - - $invoice->save(); - } else { - $invoice->delete(); - } - } - - return $this->log_deletion_action( $user_id, $invoices->invoices, $action ); - } - - /** - * Logs the deletion or anonymization action for a user and their invoices. - * - * @since 2.8.22 - * @param int $user_id The ID of the user being processed. - * @param array $invoices An array of invoice objects being processed. - * @param string $action The action being performed (anonymize or delete). - * @return bool True on success, false on failure. - */ - private function log_deletion_action( $user_id, $invoices, $action ) { - global $wpdb; - - $table_name = $wpdb->prefix . 'getpaid_anonymization_logs'; - $user_data = get_userdata( $user_id ); - - $additional_info = array( - 'Username' => $user_data ? $user_data->user_login : 'N/A', - 'User Roles' => $user_data ? implode(', ', $user_data->roles) : 'N/A', - 'Email' => $user_data ? $user_data->user_email : 'N/A', - 'First Name' => $user_data ? $user_data->first_name : 'N/A', - 'Last Name' => $user_data ? $user_data->last_name : 'N/A', - 'Registered' => $user_data ? $user_data->user_registered : 'N/A', - 'invoice_count' => count( $invoices ), - ); - - - /** - * Filters the additional info before logging. - * - * @since 2.8.22 - * @param array $additional_info The additional information to be logged. - * @param int $user_id The ID of the user being processed. - * @param array $invoices The invoices being processed. - * @param string $action The action being performed (anonymize or delete). - */ - $additional_info = apply_filters( 'wpinv_anonymization_log_additional_info', $additional_info, $user_id, $invoices, $action ); - - $data = array( - 'user_id' => $user_id, - 'action' => sanitize_text_field( $action ), - 'data_type' => 'User Invoices', - 'timestamp' => current_time( 'mysql' ), - 'additional_info' => wp_json_encode( $additional_info ), - ); - - $format = array( - '%d', // user_id - '%s', // action - '%s', // data_type - '%s', // timestamp - '%s', // additional_info - ); - - if ( ! empty( $user_id ) && ! empty( $action ) ) { - $result = $wpdb->update( - $table_name, - $data, - array( - 'user_id' => (int) $user_id, - 'action' => sanitize_text_field( $action ), - ), - $format, - array( '%d', '%s' ) - ); - - if ( false === $result ) { - // If update fails, try to insert. - $result = $wpdb->insert( $table_name, $data, $format ); - } - - if ( false === $result ) { - wpinv_error_log( sprintf( 'Failed to log anonymization action for user ID: %d. Error: %s', $user_id, $wpdb->last_error ) ); - return false; - } - } - - /** - * Fires after logging a deletion or anonymization action. - * - * @since 2.8.22 - * @param int $user_id The ID of the user being processed. - * @param array $invoices An array of invoice objects being processed. - * @param string $action The action being performed (anonymize or delete). - * @param array $data The data that was inserted into the log. - */ - do_action( 'wpinv_after_log_deletion_action', $user_id, $invoices, $action, $data ); - - return true; - } - - /** - * Handles GDPR personal data erasure request. - * - * @since 2.8.22 - * @param array $response The default response. - * @param int $user_id The ID of the user being erased. - * @return array The modified response. - */ - public function handle_erasure_request( $response, $user_id ) { - if ( $this->has_active_subscriptions( $user_id ) ) { - $response['messages'][] = esc_html__( 'User has active subscriptions. Data cannot be erased at this time.', 'invoicing' ); - $response['items_removed'] = false; - } elseif ( $this->has_paid_invoices( $user_id ) ) { - $retention_method = wpinv_get_option( 'data_retention_method', 'anonymize' ); - if ( 'anonymize' === $retention_method ) { - $this->anonymize_user_data( $user_id ); - $response['messages'][] = esc_html__( 'User data has been anonymized due to existing paid invoices.', 'invoicing' ); - $response['items_removed'] = false; - $response['items_retained'] = true; - } else { - $this->delete_user_data( $user_id ); - $response['messages'][] = esc_html__( 'User data has been deleted.', 'invoicing' ); - $response['items_removed'] = true; - $response['items_retained'] = false; - } - } - - return $response; - } - - /** - * Hashes email for anonymization. - * - * @since 2.8.22 - * @param string $email The email to hash. - * @return string The hashed email. - */ - private function hash_email( $email ) { - $site_url = get_site_url(); - $domain = wp_parse_url( $site_url, PHP_URL_HOST ); - - if ( empty( $domain ) ) { - return $email; - } - - $clean_email = sanitize_email( strtolower( trim( $email ) ) ); - $hash = wp_hash( $clean_email ); - $hash = substr( $hash, 0, 20 ); - $anonymized_email = sprintf( '%s@%s', $hash, $domain ); - - /** - * Filters the anonymized email before returning. - * - * @since 2.8.22 - * @param string $anonymized_email The anonymized email address. - * @param string $email The original email address. - */ - return apply_filters( 'wpinv_anonymized_email', $anonymized_email, $email ); - } - - /** - * Anonymizes a given piece of data. - * - * @since 2.8.22 - * @param string $data The data to anonymize. - * @return string The anonymized data. - */ - private function anonymize_data( $data ) { - if ( empty( $data ) ) { - return ''; - } - - return wp_privacy_anonymize_data( 'text', $data ); - } - - /** - * Performs data retention cleanup. - * - * This method is responsible for cleaning up anonymized user data - * that has exceeded the retention period. - * - * @since 2.8.22 - */ - public function perform_data_retention_cleanup() { - global $wpdb; - - $retention_period = wpinv_get_option( 'data_retention_period', '3650' ); - - // If retention period is set to 'never', exit the function. - if ( 'never' === $retention_period ) { - return; - } - - $customers_table = $wpdb->prefix . 'getpaid_customers'; - - // Calculate the cutoff date for data retention. - $cutoff_date = gmdate( 'Y-m-d', strtotime( "-$retention_period days" ) ); - - $expired_records = $wpdb->get_results( - $wpdb->prepare( - "SELECT * FROM $customers_table WHERE deletion_date < %s AND is_anonymized = 1", - $cutoff_date - ) - ); - - /** - * Fires before the data retention cleanup process begins. - * - * @since 2.8.22 - * @param array $expired_records Array of customer records to be processed. - */ - do_action( 'getpaid_data_retention_before_cleanup', $expired_records ); - - if ( ! empty( $expired_records ) ) { - // Disable our custom user deletion handling. - $this->handle_user_deletion = false; - - foreach ( $expired_records as $record ) { - // Delete associated invoices. - $this->ensure_invoice_anonymization( (int) $record->user_id, 'delete' ); - - // Delete the user. - wp_delete_user( (int) $record->user_id ); - - /** - * Fires after processing each expired record during cleanup. - * - * @since 2.8.22 - * @param object $record The customer record being processed. - */ - do_action( 'getpaid_data_retention_process_record', $record ); - } - - // Re-enable our custom user deletion handling. - $this->handle_user_deletion = true; - - /** - * Fires after the data retention cleanup process is complete. - * - * @since 2.8.22 - * @param array $expired_records Array of customer records that were processed. - */ - do_action( 'getpaid_data_retention_after_cleanup', $expired_records ); - } - - /** - * Fires after the data retention cleanup attempt, regardless of whether records were processed. - * - * @since 2.8.22 - * @param int $retention_period The current retention period in years. - * @param string $cutoff_date The cutoff date used for identifying expired records. - */ - do_action( 'getpaid_data_retention_cleanup_complete', $retention_period, $cutoff_date ); - } - - /** - * Ends the user's current session. - * - * @since 2.8.22 - */ - private function end_user_session() { - wp_logout(); - - // Redirect after deletion. - $redirect_page = home_url(); - wp_safe_redirect( $redirect_page ); - exit(); - } -} \ No newline at end of file + ); + + /** + * Filters the user meta fields to be anonymized. + * + * @since 2.8.22 + * @param array $user_meta_data The meta fields to be anonymized. + * @param int $user_id The ID of the user being anonymized. + */ + $user_meta_data = apply_filters( 'wpinv_user_meta_data_to_anonymize', $user_meta_data, $user->ID ); + + foreach ( $user_meta_data as $meta_key ) { + delete_user_meta( $user->ID, $meta_key ); + } + + return $this->ensure_invoice_anonymization( $user->ID, 'anonymize' ); + } + + /** + * Deletes user data without anonymization. + * + * @param int $user_id The ID of the user to delete. + * @return bool True on success, false on failure. + */ + private function delete_user_data( $user_id ) { + // Delete associated invoices. + $this->ensure_invoice_anonymization( $user_id, 'delete' ); + + // Disable our handler to prevent infinite recursion. + $this->handle_user_deletion = false; + + // Delete the user. + if ( is_multisite() ) { + wpmu_delete_user( $user_id ); + } else { + wp_delete_user( $user_id ); + } + + // Re-enable our handler. + $this->handle_user_deletion = true; + + /** + * Fires after deleting user data without anonymization. + * + * @since 2.8.22 + * @param int $user_id The ID of the user being deleted. + */ + do_action( 'wpinv_delete_user_data', $user_id ); + + return true; + } + + /** + * Ensures invoice data remains anonymized. + * + * @since 2.8.22 + * @param int $user_id The ID of the user whose invoices should be checked. + * @param string $action The action to perform (anonymize or delete). + * @return bool True on success, false on failure. + */ + public function ensure_invoice_anonymization( $user_id, $action = 'anonymize' ) { + $invoices = wpinv_get_invoices( array( 'user' => $user_id ) ); + + /** + * Filters the invoice meta fields to be anonymized. + * + * @since 2.8.22 + * @param array $inv_meta_data The meta fields to be anonymized. + * @param int $user_id The ID of the user being processed. + */ + $inv_meta_data = apply_filters( 'wpinv_invoice_meta_data_to_anonymize', array(), $user_id ); + + foreach ( $invoices->invoices as $invoice ) { + foreach ( $inv_meta_data as $meta_key ) { + delete_post_meta( $invoice->get_id(), $meta_key ); + } + + if ( 'anonymize' === $action ) { + $invoice->set_email( $this->hash_email( $invoice->get_email() ) ); + $invoice->set_email_cc( '' ); + $invoice->set_phone( '' ); + $invoice->set_ip( $this->anonymize_data( $invoice->get_ip() ) ); + $invoice->set_first_name( __( 'Anonymized', 'invoicing' ) ); + $invoice->set_last_name( '' ); + $invoice->set_company( '' ); + $invoice->set_vat_number( '' ); + $invoice->set_address( '' ); + $invoice->set_city( '' ); + $invoice->set_zip( '' ); + $invoice->set_is_anonymized( 1 ); + + /** + * Fires when anonymizing additional invoice data. + * + * @since 2.8.22 + * @param WPInv_Invoice $invoice The invoice being anonymized. + * @param string $action The action being performed (anonymize or delete). + */ + do_action( 'wpinv_anonymize_invoice_data', $invoice, $action ); + + $invoice->save(); + } else { + $invoice->delete(); + } + } + + return $this->log_deletion_action( $user_id, $invoices->invoices, $action ); + } + + /** + * Logs the deletion or anonymization action for a user and their invoices. + * + * @since 2.8.22 + * @param int $user_id The ID of the user being processed. + * @param array $invoices An array of invoice objects being processed. + * @param string $action The action being performed (anonymize or delete). + * @return bool True on success, false on failure. + */ + private function log_deletion_action( $user_id, $invoices, $action ) { + global $wpdb; + + $table_name = $wpdb->prefix . 'getpaid_anonymization_logs'; + + $additional_info = array( + 'invoice_count' => count( $invoices ), + 'retention_method' => wpinv_get_option( 'data_retention_method', 'anonymize' ), + 'retention_period' => wpinv_get_option( 'data_retention_period', '3650' ), + ); + + /** + * Filters the additional info before logging. + * + * @since 2.8.22 + * @param array $additional_info The additional information to be logged. + * @param int $user_id The ID of the user being processed. + * @param array $invoices The invoices being processed. + * @param string $action The action being performed (anonymize or delete). + */ + $additional_info = apply_filters( 'wpinv_anonymization_log_additional_info', $additional_info, $user_id, $invoices, $action ); + + $data = array( + 'user_id' => $user_id, + 'action' => sanitize_text_field( $action ), + 'data_type' => 'User Invoices', + 'timestamp' => current_time( 'mysql', true ), + 'additional_info' => wp_json_encode( $additional_info ), + ); + + $format = array( + '%d', // user_id + '%s', // action + '%s', // data_type + '%s', // timestamp + '%s', // additional_info + ); + + if ( ! empty( $user_id ) && ! empty( $action ) ) { + // Check if a log entry already exists for this user and action. + $existing = $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(*) FROM $table_name WHERE user_id = %d AND action = %s", + (int) $user_id, + sanitize_text_field( $action ) + ) + ); + + if ( $existing ) { + $result = $wpdb->update( + $table_name, + $data, + array( + 'user_id' => (int) $user_id, + 'action' => sanitize_text_field( $action ), + ), + $format, + array( '%d', '%s' ) + ); + } else { + $result = $wpdb->insert( $table_name, $data, $format ); + } + + if ( false === $result ) { + wpinv_error_log( sprintf( 'Failed to log anonymization action for user ID: %d. Error: %s', $user_id, $wpdb->last_error ) ); + return false; + } + } + + /** + * Fires after logging a deletion or anonymization action. + * + * @since 2.8.22 + * @param int $user_id The ID of the user being processed. + * @param array $invoices An array of invoice objects being processed. + * @param string $action The action being performed (anonymize or delete). + * @param array $data The data that was inserted into the log. + */ + do_action( 'wpinv_after_log_deletion_action', $user_id, $invoices, $action, $data ); + + return true; + } + + /** + * Registers the GDPR personal data eraser. + * + * @since 2.8.22 + * @param array $erasers Registered erasers. + * @return array Modified erasers. + */ + public function register_eraser( $erasers ) { + $erasers['getpaid-data-retention'] = array( + 'eraser_friendly_name' => __( 'GetPaid Invoice Data', 'invoicing' ), + 'callback' => array( $this, 'handle_erasure_request' ), + ); + + return $erasers; + } + + /** + * Handles GDPR personal data erasure request. + * + * @since 2.8.22 + * @param string $email_address The email address of the user being erased. + * @param int $page The current page (for batched processing). + * @return array The erasure response. + */ + public function handle_erasure_request( $email_address, $page = 1 ) { + $response = array( + 'items_removed' => false, + 'items_retained' => false, + 'messages' => array(), + 'done' => true, + ); + + $user = get_user_by( 'email', $email_address ); + + if ( ! $user ) { + return $response; + } + + $user_id = $user->ID; + + if ( $this->has_active_subscriptions( $user_id ) ) { + $response['messages'][] = esc_html__( 'User has active subscriptions. Data cannot be erased at this time.', 'invoicing' ); + $response['items_removed'] = false; + } elseif ( $this->has_paid_invoices( $user_id ) ) { + $retention_method = wpinv_get_option( 'data_retention_method', 'anonymize' ); + if ( 'anonymize' === $retention_method ) { + $this->anonymize_user_data( $user_id ); + $response['messages'][] = esc_html__( 'User data has been anonymized due to existing paid invoices.', 'invoicing' ); + $response['items_removed'] = false; + $response['items_retained'] = true; + } else { + $this->delete_user_data( $user_id ); + $response['messages'][] = esc_html__( 'User data has been deleted.', 'invoicing' ); + $response['items_removed'] = true; + $response['items_retained'] = false; + } + } + + return $response; + } + + /** + * Hashes email for anonymization. + * + * @since 2.8.22 + * @param string $email The email to hash. + * @return string The hashed email. + */ + private function hash_email( $email ) { + if ( empty( $email ) ) { + return ''; + } + + $site_url = get_site_url(); + $domain = wp_parse_url( $site_url, PHP_URL_HOST ); + + if ( empty( $domain ) ) { + $domain = 'localhost'; + } + + $clean_email = sanitize_email( strtolower( trim( $email ) ) ); + $hash = wp_hash( $clean_email ); + $hash = substr( $hash, 0, 20 ); + $anonymized_email = sprintf( '%s@%s', $hash, $domain ); + + /** + * Filters the anonymized email before returning. + * + * @since 2.8.22 + * @param string $anonymized_email The anonymized email address. + * @param string $email The original email address. + */ + return apply_filters( 'wpinv_anonymized_email', $anonymized_email, $email ); + } + + /** + * Anonymizes a given piece of data. + * + * @since 2.8.22 + * @param string $data The data to anonymize. + * @return string The anonymized data. + */ + private function anonymize_data( $data ) { + if ( empty( $data ) ) { + return ''; + } + + return wp_privacy_anonymize_data( 'text', $data ); + } + + /** + * Performs data retention cleanup. + * + * This method is responsible for cleaning up anonymized user data + * that has exceeded the retention period. + * + * @since 2.8.22 + */ + public function perform_data_retention_cleanup() { + global $wpdb; + + $retention_period = wpinv_get_option( 'data_retention_period', '3650' ); + + // If retention period is set to 'never', exit the function. + if ( 'never' === $retention_period ) { + return; + } + + $customers_table = $wpdb->prefix . 'getpaid_customers'; + + // Find anonymized records whose deletion_date has passed. + $expired_records = $wpdb->get_results( + $wpdb->prepare( + "SELECT * FROM $customers_table WHERE deletion_date IS NOT NULL AND deletion_date < %s AND is_anonymized = 1", + gmdate( 'Y-m-d' ) + ) + ); + + /** + * Fires before the data retention cleanup process begins. + * + * @since 2.8.22 + * @param array $expired_records Array of customer records to be processed. + */ + do_action( 'getpaid_data_retention_before_cleanup', $expired_records ); + + if ( ! empty( $expired_records ) ) { + // Disable our custom user deletion handling to prevent recursion. + $this->handle_user_deletion = false; + + foreach ( $expired_records as $record ) { + // Delete associated invoices. + $this->ensure_invoice_anonymization( (int) $record->user_id, 'delete' ); + + // Delete the user. + wp_delete_user( (int) $record->user_id ); + + /** + * Fires after processing each expired record during cleanup. + * + * @since 2.8.22 + * @param object $record The customer record being processed. + */ + do_action( 'getpaid_data_retention_process_record', $record ); + } + + // Re-enable our custom user deletion handling. + $this->handle_user_deletion = true; + + /** + * Fires after the data retention cleanup process is complete. + * + * @since 2.8.22 + * @param array $expired_records Array of customer records that were processed. + */ + do_action( 'getpaid_data_retention_after_cleanup', $expired_records ); + } + + /** + * Fires after the data retention cleanup attempt, regardless of whether records were processed. + * + * @since 2.8.22 + * @param int $retention_period The current retention period in days. + * @param string $cutoff_date The cutoff date used for identifying expired records. + */ + do_action( 'getpaid_data_retention_cleanup_complete', $retention_period, gmdate( 'Y-m-d' ) ); + } + + /** + * Ends the user's current session. + * + * @since 2.8.22 + */ + private function end_user_session() { + wp_logout(); + + // Redirect after deletion. + $redirect_page = home_url(); + wp_safe_redirect( $redirect_page ); + exit(); + } +} diff --git a/includes/gateways/class-getpaid-authorize-net-gateway.php b/includes/gateways/class-getpaid-authorize-net-gateway.php index ba5b0e03..1334c345 100644 --- a/includes/gateways/class-getpaid-authorize-net-gateway.php +++ b/includes/gateways/class-getpaid-authorize-net-gateway.php @@ -63,6 +63,24 @@ class GetPaid_Authorize_Net_Gateway extends GetPaid_Authorize_Net_Legacy_Gateway */ public $currencies = array( 'USD', 'CAD', 'GBP', 'DKK', 'NOK', 'PLN', 'SEK', 'AUD', 'EUR', 'NZD' ); + /** + * Currencies ACH payments are allowed for. + * + * @var array + */ + public $ach_currencies = array( 'USD' ); + + /** + * ACH account types. + * + * @var array + */ + protected $ach_account_types = array( + 'checking' => 'Checking', + 'savings' => 'Savings', + 'businessChecking' => 'Business Checking', + ); + /** * URL to view a transaction. * @@ -92,11 +110,27 @@ public function __construct() { */ public function payment_fields( $invoice_id, $form ) { - // Let the user select a payment method. - $this->saved_payment_methods(); + $ach_enabled = wpinv_get_option( 'authorizenet_enable_ach' ); + $show_type_selector = $ach_enabled && $this->is_ach_available(); - // Show the credit card entry form. - $this->new_payment_method_entry( $this->get_cc_form( true ) ); + // Payment type selector (CC vs ACH). + if ( $show_type_selector ) { + $this->render_payment_type_selector(); + } + + // Credit Card Section. + echo '
'; + $this->saved_payment_methods_by_type( 'card' ); + $this->new_payment_method_entry( $this->get_cc_form( true ) ); + echo '
'; + + // ACH Section. + if ( $show_type_selector ) { + echo ''; + } } /** @@ -111,8 +145,16 @@ public function payment_fields( $invoice_id, $form ) { */ public function create_customer_profile( $invoice, $submission_data, $save = true ) { - // Remove non-digits from the number - $submission_data['authorizenet']['cc_number'] = preg_replace( '/\D/', '', $submission_data['authorizenet']['cc_number'] ); + // Determine payment type. + $is_ach_payment = $this->is_ach_payment( $submission_data['authorizenet'] ); + + // Remove non-digits from the card/account number. + if ( $is_ach_payment ) { + $submission_data['authorizenet']['ach_routing_number'] = preg_replace( '/\D/', '', $submission_data['authorizenet']['ach_routing_number'] ); + $submission_data['authorizenet']['ach_account_number'] = preg_replace( '/\D/', '', $submission_data['authorizenet']['ach_account_number'] ); + } else { + $submission_data['authorizenet']['cc_number'] = preg_replace( '/\D/', '', $submission_data['authorizenet']['cc_number'] ); + } // Generate args. $args = array( @@ -169,12 +211,19 @@ public function create_customer_profile( $invoice, $submission_data, $save = tru // Save the payment token. if ( $save ) { + if ( $is_ach_payment ) { + $token_name = $this->get_ach_token_name( $submission_data['authorizenet'] ); + } else { + $token_name = getpaid_get_card_name( $submission_data['authorizenet']['cc_number'] ) . '····' . substr( $submission_data['authorizenet']['cc_number'], -4 ); + } + $this->save_token( array( - 'id' => $response->customerPaymentProfileIdList[0], - 'name' => getpaid_get_card_name( $submission_data['authorizenet']['cc_number'] ) . '····' . substr( $submission_data['authorizenet']['cc_number'], -4 ), - 'default' => true, - 'type' => $this->is_sandbox( $invoice ) ? 'sandbox' : 'live', + 'id' => $response->customerPaymentProfileIdList[0], + 'name' => $token_name, + 'default' => true, + 'type' => $this->is_sandbox( $invoice ) ? 'sandbox' : 'live', + 'payment_method' => $is_ach_payment ? 'ach' : 'card', ) ); } @@ -213,26 +262,34 @@ public function get_customer_profile( $profile_id ) { } /** - * Creates a customer profile. + * Creates a customer payment profile. * * * @param string $profile_id profile id. * @param WPInv_Invoice $invoice Invoice. * @param array $submission_data Posted checkout fields. * @param bool $save Whether or not to save the payment as a token. - * @link https://developer.authorize.net/api/reference/index.html#customer-profiles-create-customer-profile + * @link https://developer.authorize.net/api/reference/index.html#customer-profiles-create-customer-payment-profile * @return string|WP_Error Profile id. */ public function create_customer_payment_profile( $customer_profile, $invoice, $submission_data, $save ) { - // Remove non-digits from the number - $submission_data['authorizenet']['cc_number'] = preg_replace( '/\D/', '', $submission_data['authorizenet']['cc_number'] ); + // Determine payment type. + $is_ach_payment = $this->is_ach_payment( $submission_data['authorizenet'] ); + + // Remove non-digits from the card/account number. + if ( $is_ach_payment ) { + $submission_data['authorizenet']['ach_routing_number'] = preg_replace( '/\D/', '', $submission_data['authorizenet']['ach_routing_number'] ); + $submission_data['authorizenet']['ach_account_number'] = preg_replace( '/\D/', '', $submission_data['authorizenet']['ach_account_number'] ); + } else { + $submission_data['authorizenet']['cc_number'] = preg_replace( '/\D/', '', $submission_data['authorizenet']['cc_number'] ); + } - // Prepare card details. - $payment_information = $this->get_payment_information( $submission_data['authorizenet'] ); + // Prepare payment details. + $payment_information = $this->get_payment_information( $submission_data['authorizenet'] ); - // Authorize.NET does not support saving the same card twice. - $cached_information = $this->retrieve_payment_profile_from_cache( $payment_information, $customer_profile, $invoice ); + // Authorize.NET does not support saving the same payment method twice. + $cached_information = $this->retrieve_payment_profile_from_cache( $payment_information, $customer_profile, $invoice ); if ( $cached_information ) { return $cached_information; @@ -284,12 +341,19 @@ public function create_customer_payment_profile( $customer_profile, $invoice, $s // Save the payment token. if ( $save ) { + if ( $is_ach_payment ) { + $token_name = $this->get_ach_token_name( $submission_data['authorizenet'] ); + } else { + $token_name = getpaid_get_card_name( $submission_data['authorizenet']['cc_number'] ) . ' ···· ' . substr( $submission_data['authorizenet']['cc_number'], -4 ); + } + $this->save_token( array( - 'id' => $response->customerPaymentProfileId, - 'name' => getpaid_get_card_name( $submission_data['authorizenet']['cc_number'] ) . ' ···· ' . substr( $submission_data['authorizenet']['cc_number'], -4 ), - 'default' => true, - 'type' => $this->is_sandbox( $invoice ) ? 'sandbox' : 'live', + 'id' => $response->customerPaymentProfileId, + 'name' => $token_name, + 'default' => true, + 'type' => $this->is_sandbox( $invoice ) ? 'sandbox' : 'live', + 'payment_method' => $is_ach_payment ? 'ach' : 'card', ) ); } @@ -489,20 +553,24 @@ public function process_charge_response( $result, $invoice ) { * Returns payment information. * * - * @param array $card Card details. + * @param array $data Payment form data (card or ACH details). * @return array */ - public function get_payment_information( $card ) { - return array( - - 'creditCard' => array( - 'cardNumber' => $card['cc_number'], - 'expirationDate' => $card['cc_expire_year'] . '-' . $card['cc_expire_month'], - 'cardCode' => $card['cc_cvv2'], - ), + public function get_payment_information( $data ) { + // Check if this is an ACH payment. + if ( $this->is_ach_payment( $data ) ) { + return $this->get_ach_payment_information( $data ); + } - ); - } + // Default to credit card. + return array( + 'creditCard' => array( + 'cardNumber' => $data['cc_number'], + 'expirationDate' => $data['cc_expire_year'] . '-' . $data['cc_expire_month'], + 'cardCode' => $data['cc_cvv2'], + ), + ); + } /** * Returns the customer profile meta name. @@ -515,6 +583,302 @@ public function get_customer_profile_meta_name( $invoice ) { return $this->is_sandbox( $invoice ) ? 'getpaid_authorizenet_sandbox_customer_profile_id' : 'getpaid_authorizenet_customer_profile_id'; } + /** + * Checks if ACH payments are available for the current currency. + * + * @return bool + */ + public function is_ach_available() { + $currency = wpinv_get_currency(); + return in_array( $currency, $this->ach_currencies, true ); + } + + /** + * Checks if the payment data is for an ACH payment. + * + * @param array $data Payment data. + * @return bool + */ + public function is_ach_payment( $data ) { + return isset( $data['payment_type'] ) && 'ach' === $data['payment_type']; + } + + /** + * Generates a display name for an ACH token. + * + * @param array $data ACH form data. + * @return string + */ + public function get_ach_token_name( $data ) { + $account_type = isset( $data['ach_account_type'] ) ? $data['ach_account_type'] : 'checking'; + $account_number = preg_replace( '/\D/', '', isset( $data['ach_account_number'] ) ? $data['ach_account_number'] : '' ); + $last_four = substr( $account_number, -4 ); + + $type_labels = array( + 'checking' => __( 'Checking', 'invoicing' ), + 'savings' => __( 'Savings', 'invoicing' ), + 'businessChecking' => __( 'Business Checking', 'invoicing' ), + ); + + $type_label = isset( $type_labels[ $account_type ] ) ? $type_labels[ $account_type ] : __( 'Bank Account', 'invoicing' ); + + return $type_label . ' ····' . $last_four; + } + + /** + * Returns the ACH/eCheck form HTML. + * + * @param bool $save Whether to display the save checkbox. + * @return string + */ + public function get_ach_form( $save = false ) { + ob_start(); + + $id_prefix = esc_attr( uniqid( $this->id . '_ach_' ) ); + ?> +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + save_payment_method_checkbox(); ?> + + + +
+ +
+ +
+
+ +
+
+ +
+ + +
+
+ + +
+
+
+ sanitize_text_field( isset( $data['ach_account_type'] ) ? $data['ach_account_type'] : 'checking' ), + 'routingNumber' => preg_replace( '/\D/', '', isset( $data['ach_routing_number'] ) ? $data['ach_routing_number'] : '' ), + 'accountNumber' => preg_replace( '/\D/', '', isset( $data['ach_account_number'] ) ? $data['ach_account_number'] : '' ), + 'nameOnAccount' => getpaid_limit_length( sanitize_text_field( isset( $data['ach_name_on_account'] ) ? $data['ach_name_on_account'] : '' ), 22 ), + 'echeckType' => 'WEB', + ); + + return array( 'bankAccount' => $bank_account ); + } + + /** + * Validates ACH form fields. + * + * @param array $data ACH form data. + * @return true|WP_Error + */ + public function validate_ach_fields( $data ) { + // Routing number validation (9 digits). + $routing = preg_replace( '/\D/', '', isset( $data['ach_routing_number'] ) ? $data['ach_routing_number'] : '' ); + if ( strlen( $routing ) !== 9 ) { + return new WP_Error( 'invalid_routing', __( 'Please enter a valid 9-digit routing number.', 'invoicing' ) ); + } + + // Validate routing number checksum. + if ( ! $this->validate_routing_number_checksum( $routing ) ) { + return new WP_Error( 'invalid_routing', __( 'The routing number appears to be invalid.', 'invoicing' ) ); + } + + // Account number validation (1-17 digits). + $account = preg_replace( '/\D/', '', isset( $data['ach_account_number'] ) ? $data['ach_account_number'] : '' ); + if ( empty( $account ) || strlen( $account ) > 17 ) { + return new WP_Error( 'invalid_account', __( 'Please enter a valid account number.', 'invoicing' ) ); + } + + // Name on account validation. + $name = trim( isset( $data['ach_name_on_account'] ) ? $data['ach_name_on_account'] : '' ); + if ( empty( $name ) ) { + return new WP_Error( 'invalid_name', __( 'Please enter the name on the account.', 'invoicing' ) ); + } + + // Account type validation. + $valid_types = array_keys( $this->ach_account_types ); + $account_type = isset( $data['ach_account_type'] ) ? $data['ach_account_type'] : ''; + if ( ! in_array( $account_type, $valid_types, true ) ) { + return new WP_Error( 'invalid_account_type', __( 'Please select a valid account type.', 'invoicing' ) ); + } + + return true; + } + + /** + * Validates a routing number using the ABA checksum algorithm. + * + * @param string $routing The 9-digit routing number. + * @return bool + */ + public function validate_routing_number_checksum( $routing ) { + if ( strlen( $routing ) !== 9 ) { + return false; + } + + $checksum = ( + 3 * ( (int) $routing[0] + (int) $routing[3] + (int) $routing[6] ) + + 7 * ( (int) $routing[1] + (int) $routing[4] + (int) $routing[7] ) + + 1 * ( (int) $routing[2] + (int) $routing[5] + (int) $routing[8] ) + ); + + return $checksum % 10 === 0; + } + + /** + * Displays saved payment methods filtered by type. + * + * @param string $payment_type 'card' or 'ach'. + */ + public function saved_payment_methods_by_type( $payment_type = 'card' ) { + $tokens = $this->get_tokens( $this->is_sandbox() ); + + // Filter tokens by payment method type. + $filtered_tokens = array(); + foreach ( $tokens as $token ) { + $token_type = isset( $token['payment_method'] ) ? $token['payment_method'] : 'card'; + if ( $token_type === $payment_type ) { + $filtered_tokens[] = $token; + } + } + + // For cards, if no tokens have payment_method set, assume they're all cards (backwards compatibility). + if ( empty( $filtered_tokens ) && 'card' === $payment_type ) { + foreach ( $tokens as $token ) { + if ( ! isset( $token['payment_method'] ) || 'card' === $token['payment_method'] ) { + $filtered_tokens[] = $token; + } + } + } + + $input_name = 'ach' === $payment_type ? 'getpaid-authorizenet-ach-payment-method' : 'getpaid-authorizenet-payment-method'; + $new_label = 'ach' === $payment_type ? __( 'Use a new bank account', 'invoicing' ) : __( 'Use a new card', 'invoicing' ); + + echo ''; + } + /** * Validates the submitted data. * @@ -532,16 +896,31 @@ public function validate_submission_data( $submission_data, $invoice ) { return new WP_Error( 'invalid_settings', __( 'Please set-up your login id and transaction key before using this gateway.', 'invoicing' ) ); } + // Determine if this is an ACH payment. + $is_ach_payment = $this->is_ach_payment( isset( $submission_data['authorizenet'] ) ? $submission_data['authorizenet'] : array() ); + $payment_method_key = $is_ach_payment ? 'getpaid-authorizenet-ach-payment-method' : 'getpaid-authorizenet-payment-method'; + // Validate the payment method. - if ( empty( $submission_data['getpaid-authorizenet-payment-method'] ) ) { - return new WP_Error( 'invalid_payment_method', __( 'Please select a different payment method or add a new card.', 'invoicing' ) ); + if ( empty( $submission_data[ $payment_method_key ] ) ) { + $error_message = $is_ach_payment + ? __( 'Please select a different payment method or add a new bank account.', 'invoicing' ) + : __( 'Please select a different payment method or add a new card.', 'invoicing' ); + return new WP_Error( 'invalid_payment_method', $error_message ); } // Are we adding a new payment method? - if ( 'new' != $submission_data['getpaid-authorizenet-payment-method'] ) { - return $submission_data['getpaid-authorizenet-payment-method']; + if ( 'new' != $submission_data[ $payment_method_key ] ) { + return $submission_data[ $payment_method_key ]; } + // Validate ACH fields if this is a new ACH payment. + if ( $is_ach_payment ) { + $ach_validation = $this->validate_ach_fields( $submission_data['authorizenet'] ); + if ( is_wp_error( $ach_validation ) ) { + return $ach_validation; + } + } + // Retrieve the customer profile id. $profile_id = get_user_meta( $invoice->get_user_id(), $this->get_customer_profile_meta_name( $invoice ), true ); @@ -896,6 +1275,32 @@ public function admin_settings( $admin_settings ) { 'readonly' => true, ); + // ACH/eCheck Settings. + $admin_settings['authorizenet_ach_header'] = array( + 'type' => 'header', + 'id' => 'authorizenet_ach_header', + 'name' => '

' . __( 'ACH/eCheck Settings', 'invoicing' ) . '

', + ); + + $admin_settings['authorizenet_enable_ach'] = array( + 'type' => 'checkbox', + 'id' => 'authorizenet_enable_ach', + 'name' => __( 'Enable ACH Payments', 'invoicing' ), + 'desc' => sprintf( + __( 'Allow customers to pay via bank account (ACH/eCheck). Only available for USD transactions. %1$sLearn more about ACH payments%2$s.', 'invoicing' ), + '', + '' + ), + ); + + $admin_settings['authorizenet_ach_description'] = array( + 'type' => 'text', + 'id' => 'authorizenet_ach_description', + 'name' => __( 'ACH Description', 'invoicing' ), + 'desc' => __( 'Description shown to customers when they select ACH payment.', 'invoicing' ), + 'std' => __( 'Pay directly from your bank account.', 'invoicing' ), + ); + return $admin_settings; } diff --git a/readme.txt b/readme.txt index 9444bf97..9b38601a 100644 --- a/readme.txt +++ b/readme.txt @@ -142,6 +142,11 @@ Automatic updates should work seamlessly. To avoid unforeseen problems, we alway == Changelog == += 2.8.45 - TBD = +* Data anonymization - Fixed GDPR eraser hook, infinite recursion on user deletion, personal data leaking in logs, and missing billing address anonymization - FIXED +* Authorize.net - ACH/eCheck bank account payments - ADDED +* Minor bug fixes - FIXED + = 2.8.44 - 2026-02-26 = * Option added to use native site template for direct payment page - ADDED