| @@ -56,9 +56,9 @@ | ||
| 56 | 56 | */ |
| 57 | 57 | $existing = $order->get_meta( 'disco_campaign', false ); |
| 58 | 58 | |
| 59 | 59 | if ( ! empty( $existing ) ) { |
| 60 | - WC()->session->__unset( 'disco_campaign' ); | |
| 60 | + disco_finalise_campaign_usage( $order ); | |
| 61 | 61 | |
| 62 | 62 | return; |
| 63 | 63 | } |
| 64 | 64 | |
| @@ -70,8 +70,13 @@ | ||
| 70 | 70 | $order->save(); |
| 71 | 71 | |
| 72 | 72 | WC()->session->__unset( 'disco_campaign' ); |
| 73 | 73 | |
| 74 | + // Drop the cached usage counts for the campaigns this order just used. | |
| 75 | + foreach ( $campaigns as $campaign_id ) { | |
| 76 | + \Disco\App\Features\UserLimit::flush_cache( (int) $campaign_id ); | |
| 77 | + } | |
| 78 | + | |
| 74 | 79 | // Clear price cache so user limits are re-evaluated |
| 75 | 80 | if ( !function_exists( 'disco_clear_price_cache' ) ) { |
| 76 | 81 | return; |
| 77 | 82 | } |
| @@ -80,8 +85,193 @@ | ||
| 80 | 85 | } |
| 81 | 86 | |
| 82 | 87 | add_action( 'woocommerce_thankyou', 'disco_add_order_meta', PHP_INT_MAX ); |
| 83 | 88 | add_action( 'woocommerce_payment_complete', 'disco_add_order_meta', PHP_INT_MAX ); |
| 89 | +} | |
| 90 | + | |
| 91 | +if ( ! function_exists( 'disco_finalise_campaign_usage' ) ) { | |
| 92 | + | |
| 93 | + /** | |
| 94 | + * Settle the caches and session for an order that already carries its meta. | |
| 95 | + * | |
| 96 | + * Normally the meta was written by disco_add_campaign_order_meta() as the | |
| 97 | + * order was created. The usage caches have to be dropped here rather than | |
| 98 | + * there: at creation the order is not persisted yet, so a flush would | |
| 99 | + * immediately repopulate the old count from the database. | |
| 100 | + * | |
| 101 | + * @param \WC_Order $order Order object. | |
| 102 | + * @return void | |
| 103 | + */ | |
| 104 | + function disco_finalise_campaign_usage( $order ) { | |
| 105 | + foreach ( disco_campaign_ids_on_order( $order ) as $campaign_id ) { | |
| 106 | + \Disco\App\Features\UserLimit::flush_cache( $campaign_id ); | |
| 107 | + } | |
| 108 | + | |
| 109 | + if ( WC()->session ) { | |
| 110 | + WC()->session->__unset( 'disco_campaign' ); | |
| 111 | + } | |
| 112 | + | |
| 113 | + if ( ! function_exists( 'disco_clear_price_cache' ) ) { | |
| 114 | + return; | |
| 115 | + } | |
| 116 | + | |
| 117 | + disco_clear_price_cache(); | |
| 118 | + } | |
| 119 | + | |
| 120 | +} | |
| 121 | + | |
| 122 | +if ( ! function_exists( 'disco_campaign_ids_on_order' ) ) { | |
| 123 | + | |
| 124 | + /** | |
| 125 | + * Campaign IDs already recorded on an order. | |
| 126 | + * | |
| 127 | + * @param \WC_Order $order Order object. | |
| 128 | + * @return array<int, int> | |
| 129 | + */ | |
| 130 | + function disco_campaign_ids_on_order( $order ): array { | |
| 131 | + if ( ! $order instanceof WC_Order ) { | |
| 132 | + return array(); | |
| 133 | + } | |
| 134 | + | |
| 135 | + $meta = $order->get_meta( 'disco_campaign', false ); | |
| 136 | + | |
| 137 | + if ( empty( $meta ) ) { | |
| 138 | + return array(); | |
| 139 | + } | |
| 140 | + | |
| 141 | + return array_map( 'intval', wp_list_pluck( $meta, 'value' ) ); | |
| 142 | + } | |
| 143 | + | |
| 144 | +} | |
| 145 | + | |
| 146 | +if ( ! function_exists( 'disco_add_campaign_order_meta' ) ) { | |
| 147 | + | |
| 148 | + /** | |
| 149 | + * Record the applied campaigns while the order is being created. | |
| 150 | + * | |
| 151 | + * The campaign IDs are staged in the WC session as the cart is priced, and | |
| 152 | + * were previously read back on woocommerce_thankyou / payment_complete. Those | |
| 153 | + * run in a later request, which is not always the same session: an off site | |
| 154 | + * gateway can return the customer on a fresh one, and a payment webhook has | |
| 155 | + * no customer session at all. The handover then found nothing and returned | |
| 156 | + * silently, leaving a discounted order with no campaign recorded, so its | |
| 157 | + * usage limit never counted it and Analytics never saw it. | |
| 158 | + * | |
| 159 | + * Reading the session here instead keeps both halves in the request that | |
| 160 | + * priced the cart. The later hooks stay in place as a fallback for anything | |
| 161 | + * that reaches them without meta, and to drop the usage caches once the order | |
| 162 | + * actually exists. | |
| 163 | + * | |
| 164 | + * @param \WC_Order $order The order being created. | |
| 165 | + * @return void | |
| 166 | + */ | |
| 167 | + function disco_add_campaign_order_meta( $order ) { | |
| 168 | + if ( ! $order instanceof WC_Order || ! WC()->session ) { | |
| 169 | + return; | |
| 170 | + } | |
| 171 | + | |
| 172 | + $campaigns = WC()->session->get( 'disco_campaign' ); | |
| 173 | + | |
| 174 | + if ( empty( $campaigns ) || ! is_array( $campaigns ) ) { | |
| 175 | + return; | |
| 176 | + } | |
| 177 | + | |
| 178 | + $existing = disco_campaign_ids_on_order( $order ); | |
| 179 | + | |
| 180 | + foreach ( $campaigns as $campaign_id ) { | |
| 181 | + $campaign_id = (int) $campaign_id; | |
| 182 | + | |
| 183 | + if ( in_array( $campaign_id, $existing, true ) ) { | |
| 184 | + continue; | |
| 185 | + } | |
| 186 | + | |
| 187 | + $order->add_meta_data( 'disco_campaign', $campaign_id, false ); | |
| 188 | + | |
| 189 | + $existing[] = $campaign_id; | |
| 190 | + } | |
| 191 | + } | |
| 192 | + | |
| 193 | + // Classic (shortcode) checkout: order is saved by WC after this action. | |
| 194 | + add_action( 'woocommerce_checkout_create_order', 'disco_add_campaign_order_meta', 10 ); | |
| 195 | + | |
| 196 | + // Block / Store API checkout: WC_Checkout::create_order() does not run, so | |
| 197 | + // the action above never fires. | |
| 198 | + add_action( 'woocommerce_store_api_checkout_update_order_meta', 'disco_add_campaign_order_meta', 10 ); | |
| 199 | +} | |
| 200 | + | |
| 201 | +if ( ! function_exists( 'disco_flush_campaign_usage_cache_on_status_change' ) ) { | |
| 202 | + | |
| 203 | + /** | |
| 204 | + * Invalidate cached campaign usage counts when an order changes status. | |
| 205 | + * | |
| 206 | + * The usage count only counts orders in processing / on-hold / completed, so | |
| 207 | + * any status transition can change it. Flushing here keeps the cached count | |
| 208 | + * correct without querying on every cart or fragment-refresh request. | |
| 209 | + * | |
| 210 | + * @param int $order_id Order ID. | |
| 211 | + * @return void | |
| 212 | + */ | |
| 213 | + function disco_flush_campaign_usage_cache_on_status_change( $order_id ) { | |
| 214 | + $order = wc_get_order( $order_id ); | |
| 215 | + | |
| 216 | + if ( ! $order instanceof WC_Order ) { | |
| 217 | + return; | |
| 218 | + } | |
| 219 | + | |
| 220 | + $meta = $order->get_meta( 'disco_campaign', false ); | |
| 221 | + | |
| 222 | + if ( empty( $meta ) || ! is_array( $meta ) ) { | |
| 223 | + return; | |
| 224 | + } | |
| 225 | + | |
| 226 | + foreach ( wp_list_pluck( $meta, 'value' ) as $campaign_id ) { | |
| 227 | + \Disco\App\Features\UserLimit::flush_cache( (int) $campaign_id ); | |
| 228 | + } | |
| 229 | + } | |
| 230 | + | |
| 231 | + add_action( 'woocommerce_order_status_changed', 'disco_flush_campaign_usage_cache_on_status_change', 10, 1 ); | |
| 232 | + add_action( 'woocommerce_trash_order', 'disco_flush_campaign_usage_cache_on_status_change', 10, 1 ); | |
| 233 | + add_action( 'woocommerce_untrash_order', 'disco_flush_campaign_usage_cache_on_status_change', 10, 1 ); | |
| 234 | + add_action( 'woocommerce_before_delete_order', 'disco_flush_campaign_usage_cache_on_status_change', 10, 1 ); | |
| 235 | + add_action( 'woocommerce_delete_order', 'disco_flush_campaign_usage_cache_on_status_change', 10, 1 ); | |
| 236 | +} | |
| 237 | + | |
| 238 | +if ( ! function_exists( 'disco_flush_campaign_usage_cache_on_meta_write' ) ) { | |
| 239 | + | |
| 240 | + /** | |
| 241 | + * Invalidate cached campaign usage counts when disco_campaign meta is written | |
| 242 | + * outside of this plugin. | |
| 243 | + * | |
| 244 | + * Imports, migrations and third-party code write order meta directly rather | |
| 245 | + * than going through disco_add_order_meta(), so without this the cached count | |
| 246 | + * would stay stale until its TTL expired. | |
| 247 | + * | |
| 248 | + * @param int|array $meta_id Meta ID (array on delete). | |
| 249 | + * @param int $object_id Order ID. | |
| 250 | + * @param string $meta_key Meta key. | |
| 251 | + * @param mixed $meta_value Meta value. | |
| 252 | + * @return void | |
| 253 | + */ | |
| 254 | + // $meta_id and $object_id are unused but required: the meta hooks pass their | |
| 255 | + // arguments positionally, so $meta_key and $meta_value cannot be reached | |
| 256 | + // without declaring them. | |
| 257 | + function disco_flush_campaign_usage_cache_on_meta_write( $meta_id, $object_id, $meta_key, $meta_value ) { //phpcs:ignore | |
| 258 | + if ( 'disco_campaign' !== $meta_key ) { | |
| 259 | + return; | |
| 260 | + } | |
| 261 | + | |
| 262 | + \Disco\App\Features\UserLimit::flush_cache( (int) $meta_value ); | |
| 263 | + } | |
| 264 | + | |
| 265 | + // Legacy (post table) orders. | |
| 266 | + add_action( 'added_post_meta', 'disco_flush_campaign_usage_cache_on_meta_write', 10, 4 ); | |
| 267 | + add_action( 'updated_post_meta', 'disco_flush_campaign_usage_cache_on_meta_write', 10, 4 ); | |
| 268 | + add_action( 'deleted_post_meta', 'disco_flush_campaign_usage_cache_on_meta_write', 10, 4 ); | |
| 269 | + | |
| 270 | + // HPOS orders. | |
| 271 | + add_action( 'added_wc_order_meta', 'disco_flush_campaign_usage_cache_on_meta_write', 10, 4 ); | |
| 272 | + add_action( 'updated_wc_order_meta', 'disco_flush_campaign_usage_cache_on_meta_write', 10, 4 ); | |
| 273 | + add_action( 'deleted_wc_order_meta', 'disco_flush_campaign_usage_cache_on_meta_write', 10, 4 ); | |
| 84 | 274 | } |
| 85 | 275 | |
| 86 | 276 | if ( ! function_exists( 'disco_reset_campaign_session' ) ) { |
| 87 | 277 | |