0 ? $base . '_a' . $attempt : $base; } /** * Derive the AinePay userId. * * Logged-in customers are keyed on their (authenticated) WP customer id so * that balance reuse is tied to account ownership; guests get a per-order * key and therefore never reuse a balance across orders. Both are hashed * with a per-site namespace so no raw identifier or PII leaves the site, and * the "guest_order_" prefix namespaces the two so a customer id can never * collide with an order id of the same numeric value. * * The namespace MUST be a stable, per-site token (site_namespace()), not * merchant-scoped: WP customer ids are per-site, so two stores sharing one * merchant/API key would otherwise map their respective customer #42 to the * same userId and let one claim the other's reusable balance. It must also * be URL-independent so a later http→https or domain change does not strand * customers' reusable balances. Callers pass site_namespace(). * * @param string $namespace Per-site namespace (Ainepay_Order_Helper::site_namespace()). * @param int|string $customer_id WP customer id (0/empty for guests). * @param int|string $wc_order_id WooCommerce order id (guest fallback). * @return string */ public static function derive_user_id( $namespace, $customer_id, $wc_order_id ) { if ( (int) $customer_id > 0 ) { return hash( 'sha256', $namespace . '|' . (string) (int) $customer_id ); } // Guests get a per-order key and therefore never reuse a balance across // orders. The trailing "|" marks the identifier as a guest so the AinePay // backend can warn the merchant at refund time that a refunded guest balance cannot // be spent again (the backend does not enforce this — it is an advisory). // The marker is appended AFTER hashing on purpose: the hash is opaque hex // with no "|", so "userId contains |" is an unambiguous guest test, whereas // the pre-hash "guest_order_" prefix is invisible once digested. return hash( 'sha256', $namespace . '|guest_order_' . $wc_order_id ) . '|'; } /** * Whether an order's AinePay balance can be reused on a later order. * * Reuse requires an account-derived userId, which is recorded at order * placement in the _ainepay_account_user meta (see Ainepay_Gateway:: * process_payment). Reading the recorded flag — rather than re-checking * get_customer_id() — keeps the messaging consistent with the identity * that was actually used, even if the order is later linked to an account. * Falls back to the live customer id for orders placed before this meta * existed. * * @param WC_Order $order WooCommerce order. * @return bool */ public static function can_reuse_balance( $order ) { $flag = $order->get_meta( '_ainepay_account_user' ); if ( '' !== (string) $flag ) { return '1' === (string) $flag; } return $order->get_customer_id() > 0; } /** * Whether the store currency is supported for 1:1 stablecoin settlement. * * @param string|null $currency Currency code; defaults to store currency. * @return bool */ public static function is_supported_currency( $currency = null ) { if ( null === $currency ) { $currency = function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : ''; } return in_array( strtoupper( (string) $currency ), self::$supported_currencies, true ); } /** * Format an amount as the AinePay qty string: fixed 2 decimals, dot separator. * * @param float|string $amount Order total. * @return string */ public static function format_qty( $amount ) { return number_format( (float) $amount, 2, '.', '' ); } /** * Validate that an identifier satisfies the backend length constraints. * * @param string $id Identifier. * @return bool */ public static function is_valid_identifier( $id ) { $len = strlen( (string) $id ); return $len >= self::ID_MIN_LEN && $len <= self::ID_MAX_LEN; } }