Prepare the project for production deployment and final client
acceptance testing. Adds preflight fail-fast command, hardened FX rate
importer, currency seeder, deploy scripts, E2E smoke test, and the
43-item UAT checklist document with sign-off form.
Key changes:
- Phase 3.9.1 — hotfix (AddressBookEntry):
- Add $attributes defaults (use_count=0, is_default=false) so
instance values are populated right after create() without
refresh(). Resolves the red test
`it_increments_use_count_and_sets_last_used_at`.
- Phase 3.10 — deploy infrastructure (Laravel):
- Add `ifnex:preflight` command: fail-fast audit of production
environment (APP_ENV, APP_DEBUG, APP_KEY, IFNEX_API_KEY,
IFNEX_BRIDGE_API_KEY, ZARINPAL_SANDBOX, DB_CONNECTION, CORS,
currencies seeded, PHP extensions, storage writable). Exits 1
if any check fails; supports --skip-db for pre-migrate runs.
- Add `ifnex:import-fx-rates` command: replacement for
`ifnex:update-rates`. Fetches USD/AED from Exir.io + EUR/CNY
from FreeCurrencyAPI. Supports --auto-approve (cron use) and
--threshold (max change % for auto-approve). Logs critical on
HTTP failure. URL configurable via EXIR_API_URL env.
- Add `CurrencySeeder`: creates 5 base currencies (USD/AED/EUR/
CNY/IRR) with rate=0 (IRR=1). Idempotent — does not overwrite
real rates already set by admin or cron.
- Update `DatabaseSeeder`: skip super_admin with password
'password' in production (still created in local/testing).
- Fix `bootstrap/app.php`: remove dead `use ApiKeyAuth` import
(actual class is ApiKeyMiddleware). Latent fatal if api_key
alias was used.
- Add `config/ifnex.php` exir block + preflight config.
- Harden `.env.example`: MySQL default, EXIR_API_URL,
SANCTUM_STATEFUL_DOMAINS, APP_TIMEZONE, LOG_DAILY_DAYS, fix
duplicate CURRENCY_API_KEY.
- Phase 3.10 — deploy scripts (Linux):
- Add `scripts/deploy.sh`: idempotent 13-step deploy (composer
install → key:generate → preflight → migrate → seed → FX import
→ cache → chown). Fail-fast with set -e.
- Add `scripts/cron-exir-rates.sh`: daily cron wrapper for
`ifnex:import-fx-rates --source=all --auto-approve`.
- Phase 3.10.1 — preflight hotfix:
- Make expected DB connection configurable
(ifnex.preflight.expected_db_connection) instead of hardcoded
'mysql'. Prevents cascade test failure when tests would have to
mutate database.default to mysql in a sqlite test environment
(Unknown database ':memory:' → broken connection pool → 22
previously-green tests fail with "There is already an active
transaction").
- Rewrite IfnexPreflightTest to use config() rather than mutating
database.default.
- Phase 3.11 — UAT:
- Add `ifnex:smoke-test` command: E2E post-deploy automated test
(25+ checks: DB connection, core tables, currencies with
non-zero rates, countries count, roles, admin user, system
settings, storage writable, config/route cache, 4 API
endpoints: /up, /countries, /track/{invalid}, /customer/profile
without token).
- Add 9 feature tests for smoke-test (PASS + 6 FAIL + custom URL
+ 422/500 handling + auth bypass detection).
- Add `UAT-3.11.md`: 43-item Persian checklist for client
acceptance testing across 15 feature areas. Includes sign-off
form for both parties (client + VernaSoft).
- Update `CLIENT_DELIVERY.md` to record phases 3.10 and 3.11.
Tests:
- Total: 51 → 82 tests, 150 → 220+ assertions, all green.
- New: IfnexPreflightTest (9), IfnexImportFxRatesTest (9),
CurrencySeederTest (4), IfnexSmokeTestTest (9) = 31 new tests.
Documentation:
- DEPLOYMENT_HANDOFF.md §2.4: add CurrencySeeder + import-fx-rates.
- DEPLOYMENT_HANDOFF.md §2.8: update cron to use cron-exir-rates.sh.
- UAT-3.11.md: new 43-item client acceptance checklist + sign-off.
- CLIENT_DELIVERY.md: record phases 3.10 and 3.11 completion.
183 lines
6.0 KiB
PHP
183 lines
6.0 KiB
PHP
<?php
|
|
/**
|
|
* List Table API: WP_Privacy_Data_Removal_Requests_List_Table class
|
|
*
|
|
* @package WordPress
|
|
* @subpackage Administration
|
|
* @since 4.9.6
|
|
*/
|
|
|
|
if ( ! class_exists( 'WP_Privacy_Requests_Table' ) ) {
|
|
require_once ABSPATH . 'wp-admin/includes/class-wp-privacy-requests-table.php';
|
|
}
|
|
|
|
/**
|
|
* WP_Privacy_Data_Removal_Requests_List_Table class.
|
|
*
|
|
* @since 4.9.6
|
|
*/
|
|
class WP_Privacy_Data_Removal_Requests_List_Table extends WP_Privacy_Requests_Table {
|
|
/**
|
|
* Action name for the requests this table will work with.
|
|
*
|
|
* @since 4.9.6
|
|
*
|
|
* @var string $request_type Name of action.
|
|
*/
|
|
protected $request_type = 'remove_personal_data';
|
|
|
|
/**
|
|
* Post type for the requests.
|
|
*
|
|
* @since 4.9.6
|
|
*
|
|
* @var string $post_type The post type.
|
|
*/
|
|
protected $post_type = 'user_request';
|
|
|
|
/**
|
|
* Outputs the Actions column.
|
|
*
|
|
* @since 4.9.6
|
|
*
|
|
* @param WP_User_Request $item Item being shown.
|
|
* @return string Email column markup.
|
|
*/
|
|
public function column_email( $item ) {
|
|
$row_actions = array();
|
|
|
|
// Allow the administrator to "force remove" the personal data even if confirmation has not yet been received.
|
|
$status = $item->status;
|
|
$request_id = $item->ID;
|
|
$row_actions = array();
|
|
if ( 'request-confirmed' !== $status ) {
|
|
/** This filter is documented in wp-admin/includes/ajax-actions.php */
|
|
$erasers = apply_filters( 'wp_privacy_personal_data_erasers', array() );
|
|
$erasers_count = count( $erasers );
|
|
$nonce = wp_create_nonce( 'wp-privacy-erase-personal-data-' . $request_id );
|
|
|
|
$remove_data_markup = '<span class="remove-personal-data force-remove-personal-data" ' .
|
|
'data-erasers-count="' . esc_attr( $erasers_count ) . '" ' .
|
|
'data-request-id="' . esc_attr( $request_id ) . '" ' .
|
|
'data-nonce="' . esc_attr( $nonce ) .
|
|
'">';
|
|
|
|
$remove_data_markup .= '<span class="remove-personal-data-idle"><button type="button" class="button-link remove-personal-data-handle">' . __( 'Force erase personal data' ) . '</button></span>' .
|
|
'<span class="remove-personal-data-processing hidden">' . __( 'Erasing data...' ) . ' <span class="erasure-progress"></span></span>' .
|
|
'<span class="remove-personal-data-success hidden">' . __( 'Erasure completed.' ) . '</span>' .
|
|
'<span class="remove-personal-data-failed hidden">' . __( 'Force erasure has failed.' ) . ' <button type="button" class="button-link remove-personal-data-handle">' . __( 'Retry' ) . '</button></span>';
|
|
|
|
$remove_data_markup .= '</span>';
|
|
|
|
$row_actions['remove-data'] = $remove_data_markup;
|
|
}
|
|
|
|
if ( 'request-completed' !== $status ) {
|
|
$complete_request_markup = '<span>';
|
|
$complete_request_markup .= sprintf(
|
|
'<a href="%s" class="complete-request" aria-label="%s">%s</a>',
|
|
esc_url(
|
|
wp_nonce_url(
|
|
add_query_arg(
|
|
array(
|
|
'action' => 'complete',
|
|
'request_id' => array( $request_id ),
|
|
),
|
|
admin_url( 'erase-personal-data.php' )
|
|
),
|
|
'bulk-privacy_requests'
|
|
)
|
|
),
|
|
esc_attr(
|
|
sprintf(
|
|
/* translators: %s: Request email. */
|
|
__( 'Mark export request for “%s” as completed.' ),
|
|
$item->email
|
|
)
|
|
),
|
|
__( 'Complete request' )
|
|
);
|
|
$complete_request_markup .= '</span>';
|
|
}
|
|
|
|
if ( ! empty( $complete_request_markup ) ) {
|
|
$row_actions['complete-request'] = $complete_request_markup;
|
|
}
|
|
|
|
return sprintf( '<a href="%1$s">%2$s</a> %3$s', esc_url( 'mailto:' . $item->email ), esc_html( $item->email ), $this->row_actions( $row_actions ) );
|
|
}
|
|
|
|
/**
|
|
* Outputs the Next steps column.
|
|
*
|
|
* @since 4.9.6
|
|
*
|
|
* @param WP_User_Request $item Item being shown.
|
|
*/
|
|
public function column_next_steps( $item ) {
|
|
$status = $item->status;
|
|
|
|
switch ( $status ) {
|
|
case 'request-pending':
|
|
esc_html_e( 'Waiting for confirmation' );
|
|
break;
|
|
case 'request-confirmed':
|
|
/** This filter is documented in wp-admin/includes/ajax-actions.php */
|
|
$erasers = apply_filters( 'wp_privacy_personal_data_erasers', array() );
|
|
$erasers_count = count( $erasers );
|
|
$request_id = $item->ID;
|
|
$nonce = wp_create_nonce( 'wp-privacy-erase-personal-data-' . $request_id );
|
|
|
|
echo '<div class="remove-personal-data" ' .
|
|
'data-force-erase="1" ' .
|
|
'data-erasers-count="' . esc_attr( $erasers_count ) . '" ' .
|
|
'data-request-id="' . esc_attr( $request_id ) . '" ' .
|
|
'data-nonce="' . esc_attr( $nonce ) .
|
|
'">';
|
|
|
|
?>
|
|
<span class="remove-personal-data-idle"><button type="button" class="button-link remove-personal-data-handle"><?php _e( 'Erase personal data' ); ?></button></span>
|
|
<span class="remove-personal-data-processing hidden"><?php _e( 'Erasing data...' ); ?> <span class="erasure-progress"></span></span>
|
|
<span class="remove-personal-data-success success-message hidden" ><?php _e( 'Erasure completed.' ); ?></span>
|
|
<span class="remove-personal-data-failed hidden"><?php _e( 'Data erasure has failed.' ); ?> <button type="button" class="button-link remove-personal-data-handle"><?php _e( 'Retry' ); ?></button></span>
|
|
<?php
|
|
|
|
echo '</div>';
|
|
|
|
break;
|
|
case 'request-failed':
|
|
echo '<button type="submit" class="button-link" name="privacy_action_email_retry[' . $item->ID . ']" id="privacy_action_email_retry[' . $item->ID . ']">' . __( 'Retry' ) . '</button>';
|
|
break;
|
|
case 'request-completed':
|
|
echo '<a href="' . esc_url(
|
|
wp_nonce_url(
|
|
add_query_arg(
|
|
array(
|
|
'action' => 'delete',
|
|
'request_id' => array( $item->ID ),
|
|
),
|
|
admin_url( 'erase-personal-data.php' )
|
|
),
|
|
'bulk-privacy_requests'
|
|
)
|
|
) . '">' . esc_html__( 'Remove request' ) . '</a>';
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a clean label for the primary (Requester) column's row header `aria-label`.
|
|
*
|
|
* Provides screen readers with just the item email as the row header name,
|
|
* preventing them from computing the name from the full cell content.
|
|
*
|
|
* @since 7.1.0
|
|
*
|
|
* @param WP_User_Request $item Item being shown.
|
|
* @return string The user request item email.
|
|
*/
|
|
protected function get_primary_column_aria_label( $item ) {
|
|
return $item->email;
|
|
}
|
|
}
|