PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / classes / payouts-repository.php

payouts-repository.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/classes/payouts-repository.php

230 lines 8.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace StoreEngine\Classes;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 /**
10 * CRUD + migration for the unified `wp_storeengine_payouts` ledger.
11 *
12 * Each addon writes through this repository with a `payee_type`
13 * discriminator (`affiliate`, `vendor`, …). Reads filter on the same
14 * column via the (payee_type, payee_id) index.
15 */
16 final class PayoutsRepository {
17
18 const TYPE_AFFILIATE = 'affiliate';
19 const TYPE_VENDOR = 'vendor';
20
21 const MIGRATION_OPTION = 'storeengine_payouts_migration_v1';
22
23 const STATUS_PENDING = 'pending';
24 const STATUS_PROCESSING = 'processing';
25 const STATUS_PAID = 'paid';
26 const STATUS_FAILED = 'failed';
27 const STATUS_CANCELLED = 'cancelled';
28
29 public static function table(): string {
30 global $wpdb;
31 return $wpdb->prefix . 'storeengine_payouts';
32 }
33
34 /**
35 * @return int Inserted payout id, or 0 on failure.
36 */
37 public static function create( string $payee_type, int $payee_id, array $data ): int {
38 global $wpdb;
39
40 $row = [
41 'payee_type' => $payee_type,
42 'payee_id' => $payee_id,
43 'amount' => isset( $data['amount'] ) ? (float) $data['amount'] : 0.0,
44 'payment_method' => isset( $data['payment_method'] ) ? (string) $data['payment_method'] : null,
45 'reference' => isset( $data['reference'] ) ? (string) $data['reference'] : '',
46 'status' => isset( $data['status'] ) ? (string) $data['status'] : self::STATUS_PENDING,
47 'notes' => $data['notes'] ?? null,
48 'meta_json' => isset( $data['meta_json'] )
49 ? ( is_string( $data['meta_json'] ) ? $data['meta_json'] : wp_json_encode( $data['meta_json'] ) )
50 : null,
51 'created_at' => $data['created_at'] ?? current_time( 'mysql', 1 ),
52 'paid_at' => $data['paid_at'] ?? null,
53 ];
54
55 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
56 $ok = $wpdb->insert( self::table(), $row );
57 // phpcs:enable
58
59 return $ok ? (int) $wpdb->insert_id : 0;
60 }
61
62 public static function update( int $id, array $data ): bool {
63 if ( $id <= 0 ) return false;
64
65 global $wpdb;
66 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
67 $ok = $wpdb->update( self::table(), $data, [ 'id' => $id ] );
68 // phpcs:enable
69 return false !== $ok;
70 }
71
72 public static function get( int $id ): ?object {
73 if ( $id <= 0 ) return null;
74
75 global $wpdb;
76 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
77 $row = $wpdb->get_row( $wpdb->prepare(
78 'SELECT * FROM %i WHERE id = %d',
79 self::table(),
80 $id
81 ) );
82 // phpcs:enable
83 return $row ?: null;
84 }
85
86 /**
87 * @param array $filters status, search, per_page, page, count
88 */
89 public static function find_for( string $payee_type, int $payee_id = 0, array $filters = [] ): array {
90 global $wpdb;
91
92 $where = [ 'payee_type = %s' ];
93 $values = [ $payee_type ];
94
95 if ( $payee_id > 0 ) {
96 $where[] = 'payee_id = %d';
97 $values[] = $payee_id;
98 }
99
100 if ( ! empty( $filters['status'] ) && 'any' !== $filters['status'] ) {
101 $where[] = 'status = %s';
102 $values[] = (string) $filters['status'];
103 }
104
105 $where_sql = ' WHERE ' . implode( ' AND ', $where );
106
107 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $where_sql built from literal clauses only; identifier via %i and every value bound through prepare()'s spread $values (dynamic placeholder count is not statically countable).
108 if ( ! empty( $filters['count'] ) ) {
109 return [ (int) $wpdb->get_var( $wpdb->prepare(
110 'SELECT COUNT(*) FROM %i' . $where_sql,
111 self::table(),
112 ...$values
113 ) ) ];
114 }
115
116 $per_page = max( 1, (int) ( $filters['per_page'] ?? 50 ) );
117 $page = max( 1, (int) ( $filters['page'] ?? 1 ) );
118 $offset = ( $page - 1 ) * $per_page;
119
120 $values[] = $per_page;
121 $values[] = $offset;
122
123 $rows = $wpdb->get_results( $wpdb->prepare(
124 'SELECT * FROM %i' . $where_sql . ' ORDER BY created_at DESC LIMIT %d OFFSET %d',
125 self::table(),
126 ...$values
127 ) );
128 // phpcs:enable
129
130 return is_array( $rows ) ? $rows : [];
131 }
132
133 /**
134 * Delete a payout — used by the "rollback" admin action only.
135 */
136 public static function delete( int $id ): bool {
137 if ( $id <= 0 ) return false;
138 global $wpdb;
139 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
140 $ok = $wpdb->delete( self::table(), [ 'id' => $id ] );
141 // phpcs:enable
142 return (bool) $ok;
143 }
144
145 /**
146 * One-shot copy of legacy `wp_affiliate_payouts` and `wp_vendor_payouts`
147 * (and their accidental `storeengine_*` siblings, if present) into the
148 * unified ledger. Idempotent via `storeengine_payouts_migration_v1`.
149 */
150 public static function migrate_legacy_tables(): void {
151 if ( get_option( self::MIGRATION_OPTION ) ) {
152 return;
153 }
154
155 global $wpdb;
156 $prefix = $wpdb->prefix;
157
158 // Try both naming variants — older code accidentally created tables
159 // under both `affiliate_payouts` and `storeengine_affiliate_payouts`.
160 $affiliate_sources = [ $prefix . 'storeengine_affiliate_payouts', $prefix . 'affiliate_payouts' ];
161 foreach ( $affiliate_sources as $src ) {
162 if ( ! self::table_exists( $src ) ) continue;
163
164 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
165 $rows = $wpdb->get_results( "SELECT * FROM {$src}", ARRAY_A );
166 // phpcs:enable
167 if ( ! is_array( $rows ) ) continue;
168
169 foreach ( $rows as $r ) {
170 self::create( self::TYPE_AFFILIATE, (int) ( $r['affiliate_id'] ?? 0 ), [
171 'amount' => (float) ( $r['payout_amount'] ?? 0 ),
172 'payment_method' => isset( $r['payment_method'] ) ? self::normalize_payment_method( (string) $r['payment_method'] ) : null,
173 'reference' => (string) ( $r['transaction_id'] ?? '' ),
174 'status' => self::map_legacy_status( (string) ( $r['status'] ?? 'pending' ) ),
175 'created_at' => $r['created_at'] ?? null,
176 ] );
177 }
178 // First source that yielded rows wins; don't double-import.
179 break;
180 }
181
182 $vendor_sources = [ $prefix . 'storeengine_vendor_payouts', $prefix . 'vendor_payouts' ];
183 foreach ( $vendor_sources as $src ) {
184 if ( ! self::table_exists( $src ) ) continue;
185
186 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
187 $rows = $wpdb->get_results( "SELECT * FROM {$src}", ARRAY_A );
188 // phpcs:enable
189 if ( ! is_array( $rows ) ) continue;
190
191 foreach ( $rows as $r ) {
192 self::create( self::TYPE_VENDOR, (int) ( $r['user_id'] ?? 0 ), [
193 'amount' => (float) ( $r['amount'] ?? 0 ),
194 'reference' => (string) ( $r['reference'] ?? '' ),
195 'status' => (string) ( $r['status'] ?? 'pending' ),
196 'notes' => $r['notes'] ?? null,
197 'created_at' => $r['created_at'] ?? null,
198 'paid_at' => $r['paid_at'] ?? null,
199 ] );
200 }
201 break;
202 }
203
204 update_option( self::MIGRATION_OPTION, 1, false );
205 }
206
207 protected static function table_exists( string $table ): bool {
208 global $wpdb;
209 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
210 $found = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
211 // phpcs:enable
212 return $found === $table;
213 }
214
215 protected static function normalize_payment_method( string $raw ): string {
216 // Affiliate's old ENUM values: 'PayPal','Bank Transfer','Stripe','Check Payment','E-Check'.
217 $slug = strtolower( str_replace( [ ' ', '-' ], '_', trim( $raw ) ) );
218 return preg_replace( '/[^a-z0-9_]/', '', $slug );
219 }
220
221 protected static function map_legacy_status( string $raw ): string {
222 $s = strtolower( trim( $raw ) );
223 if ( 'completed' === $s ) return self::STATUS_PAID;
224 if ( in_array( $s, [ 'pending', 'paid', 'failed', 'cancelled', 'processing' ], true ) ) {
225 return $s;
226 }
227 return self::STATUS_PENDING;
228 }
229 }
230