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.
517 lines
8.6 KiB
CSS
517 lines
8.6 KiB
CSS
/*! This file is auto-generated */
|
|
html,
|
|
body {
|
|
height: 100%;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
background: #f0f0f1;
|
|
min-width: 0;
|
|
color: #3c434a;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
a {
|
|
color: var(--wp-admin-theme-color-darker-10);
|
|
transition-property: border, background, color;
|
|
transition-duration: .05s;
|
|
transition-timing-function: ease-in-out;
|
|
text-underline-position: under;
|
|
}
|
|
|
|
a {
|
|
outline: 0;
|
|
}
|
|
|
|
a:hover,
|
|
a:active {
|
|
color: var(--wp-admin-theme-color-darker-20);
|
|
}
|
|
|
|
a:focus {
|
|
color: var(--wp-admin-theme-color-darker-20);
|
|
box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9);
|
|
/* Only visible in Windows High Contrast mode */
|
|
outline: 2px solid transparent;
|
|
}
|
|
|
|
p {
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.login .message,
|
|
.login .notice,
|
|
.login .success {
|
|
border-right: 4px solid #3858e9;
|
|
padding: 8px 12px;
|
|
margin-top: 0;
|
|
margin-right: 0;
|
|
margin-bottom: 20px;
|
|
background-color: #fff;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.login .message p,
|
|
.login .notice p,
|
|
.login .success p {
|
|
font-size: 13px;
|
|
line-height: 1.54;
|
|
margin: 0.5em 0;
|
|
}
|
|
|
|
.login .success {
|
|
border-right-color: #4ab866;
|
|
background-color: #eff9f1;
|
|
}
|
|
|
|
/* Match border color from common.css */
|
|
.login .notice-error {
|
|
border-right-color: #cc1818;
|
|
background-color: #fcf0f0;
|
|
}
|
|
|
|
.login .login-error-list {
|
|
list-style: none;
|
|
}
|
|
|
|
.login .login-error-list li + li {
|
|
margin-top: 4px;
|
|
}
|
|
|
|
#loginform p.submit,
|
|
.login-action-lostpassword p.submit {
|
|
border: none;
|
|
margin: -10px 0 20px; /* May want to revisit this */
|
|
}
|
|
|
|
.login * {
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.login .input::-ms-clear {
|
|
display: none;
|
|
}
|
|
|
|
.login .pw-weak {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
/* rtl:ignore */
|
|
.login .button.wp-hide-pw {
|
|
background: transparent;
|
|
border: 1px solid transparent;
|
|
box-shadow: none;
|
|
font-size: 14px;
|
|
line-height: normal;
|
|
width: 2.5rem;
|
|
height: 2.5rem;
|
|
min-width: 40px;
|
|
min-height: 40px;
|
|
margin: 0;
|
|
padding: 5px 9px;
|
|
position: absolute;
|
|
right: 0;
|
|
top: 0;
|
|
}
|
|
|
|
.login .button.wp-hide-pw:hover {
|
|
background: transparent;
|
|
}
|
|
|
|
.login .button.wp-hide-pw:focus {
|
|
background: transparent;
|
|
border-color: var(--wp-admin-theme-color);
|
|
box-shadow: 0 0 0 1px var(--wp-admin-theme-color);
|
|
/* Only visible in Windows High Contrast mode */
|
|
outline: 2px solid transparent;
|
|
}
|
|
|
|
.login .button.wp-hide-pw:active {
|
|
background: transparent;
|
|
box-shadow: none;
|
|
transform: none;
|
|
}
|
|
|
|
.login .button.wp-hide-pw .dashicons {
|
|
width: 1.25rem;
|
|
height: 1.25rem;
|
|
}
|
|
|
|
.login .wp-pwd {
|
|
position: relative;
|
|
}
|
|
|
|
.no-js .hide-if-no-js {
|
|
display: none;
|
|
}
|
|
|
|
.login form {
|
|
margin: 24px 0;
|
|
padding: 26px 24px;
|
|
font-weight: 400;
|
|
overflow: hidden;
|
|
background: #fff;
|
|
border: 1px solid #c3c4c7;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.login form.shake {
|
|
animation: shake 0.2s cubic-bezier(.19,.49,.38,.79) both;
|
|
animation-iteration-count: 3;
|
|
transform: translateX(0);
|
|
}
|
|
|
|
@keyframes shake {
|
|
25% {
|
|
transform: translateX(20px);
|
|
}
|
|
75% {
|
|
transform: translateX(-20px);
|
|
}
|
|
100% {
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.login form.shake {
|
|
animation: none;
|
|
transform: none;
|
|
}
|
|
}
|
|
|
|
.login-action-confirm_admin_email #login {
|
|
width: 60vw;
|
|
max-width: 650px;
|
|
margin-top: -2vh;
|
|
}
|
|
|
|
@media screen and (max-width: 782px) {
|
|
.login-action-confirm_admin_email #login {
|
|
box-sizing: border-box;
|
|
margin-top: 0;
|
|
padding-right: 4vw;
|
|
padding-left: 4vw;
|
|
width: 100vw;
|
|
}
|
|
}
|
|
|
|
.login form .forgetmenot {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
/* Matches the height of the Log In button beside it, so the row centers on it. */
|
|
min-height: 40px;
|
|
font-weight: 400;
|
|
float: right;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
/* Centering aligns margin boxes, so margins left from the inline layout would offset these. */
|
|
.login form .forgetmenot input[type="checkbox"],
|
|
.login form .forgetmenot label {
|
|
margin-top: 0;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.login .button-primary {
|
|
float: left;
|
|
}
|
|
|
|
.login .reset-pass-submit {
|
|
display: flex;
|
|
flex-flow: row wrap;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.login .reset-pass-submit .button {
|
|
display: inline-block;
|
|
float: none;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.login .admin-email-confirm-form .submit {
|
|
text-align: center;
|
|
}
|
|
|
|
.admin-email__later {
|
|
text-align: right;
|
|
}
|
|
|
|
.login form p.admin-email__details {
|
|
margin: 1.1em 0;
|
|
}
|
|
|
|
.login .admin-email__heading {
|
|
border-bottom: 1px #f0f0f1 solid;
|
|
color: #50575e;
|
|
font-weight: normal;
|
|
padding-bottom: 0.5em;
|
|
text-align: right;
|
|
}
|
|
|
|
.admin-email__actions div {
|
|
padding-top: 1.5em;
|
|
}
|
|
|
|
.login .admin-email__actions .button-primary {
|
|
float: none;
|
|
margin-right: 0.25em;
|
|
margin-left: 0.25em;
|
|
}
|
|
|
|
#login form p {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
#login form .indicator-hint,
|
|
#login #reg_passmail {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
#login form p.submit {
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.login label {
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
display: inline-block;
|
|
margin-bottom: 3px;
|
|
}
|
|
|
|
.login .forgetmenot label,
|
|
.login .pw-weak label {
|
|
line-height: 1.5;
|
|
vertical-align: baseline;
|
|
}
|
|
|
|
.login h1 {
|
|
text-align: center;
|
|
}
|
|
|
|
.login h1 a {
|
|
background-image: url(../images/w-logo-gray.png?ver=20260303);
|
|
background-image: none, url(../images/wordpress-logo-gray.svg?ver=20260303);
|
|
background-size: 84px;
|
|
background-position: center top;
|
|
background-repeat: no-repeat;
|
|
color: #3c434a;
|
|
height: 84px;
|
|
font-size: 20px;
|
|
font-weight: 400;
|
|
line-height: 1.3;
|
|
margin: 0 auto 24px;
|
|
padding: 0;
|
|
text-decoration: none;
|
|
width: 84px;
|
|
text-indent: -9999px;
|
|
outline: none;
|
|
overflow: hidden;
|
|
display: block;
|
|
}
|
|
|
|
#login {
|
|
width: 320px;
|
|
padding: 5% 0 0;
|
|
margin: auto;
|
|
}
|
|
|
|
.login #nav,
|
|
.login #backtoblog {
|
|
font-size: 13px;
|
|
padding: 0 24px;
|
|
}
|
|
|
|
.login #nav {
|
|
margin: 24px 0 0;
|
|
}
|
|
|
|
#backtoblog {
|
|
margin: 16px 0;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.login #nav a,
|
|
.login #backtoblog a {
|
|
color: #50575e;
|
|
}
|
|
|
|
.login #nav a:hover,
|
|
.login #backtoblog a:hover,
|
|
.login h1 a:hover {
|
|
color: var(--wp-admin-theme-color-darker-20);
|
|
}
|
|
|
|
.login #nav a:focus,
|
|
.login #backtoblog a:focus,
|
|
.login h1 a:focus {
|
|
color: var(--wp-admin-theme-color-darker-20);
|
|
}
|
|
|
|
.login .privacy-policy-page-link {
|
|
text-align: center;
|
|
width: 100%;
|
|
margin: 3em 0 2em;
|
|
}
|
|
|
|
.login form .input,
|
|
.login input[type="text"],
|
|
.login input[type="password"] {
|
|
font-size: 24px;
|
|
line-height: 1.33333333; /* 32px */
|
|
width: 100%;
|
|
border-width: 0.0625rem;
|
|
padding: 0.1875rem 0.3125rem; /* 3px 5px */
|
|
margin: 0 0 16px 6px;
|
|
min-height: 40px;
|
|
max-height: none;
|
|
}
|
|
|
|
.login input.password-input {
|
|
font-family: Consolas, Monaco, monospace;
|
|
}
|
|
|
|
/* rtl:ignore */
|
|
.js.login input.password-input {
|
|
padding-right: 2.5rem;
|
|
}
|
|
|
|
.js.login-action-resetpass input[type="text"],
|
|
.js.login-action-resetpass input[type="password"],
|
|
.js.login-action-rp input[type="text"],
|
|
.js.login-action-rp input[type="password"] {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.login #pass-strength-result {
|
|
font-weight: 600;
|
|
margin: -1px 0 16px 5px;
|
|
padding: 6px 5px;
|
|
text-align: center;
|
|
width: 100%;
|
|
}
|
|
|
|
body.interim-login {
|
|
height: auto;
|
|
}
|
|
|
|
.interim-login #login {
|
|
padding: 0;
|
|
margin: 5px auto 20px;
|
|
}
|
|
|
|
.interim-login.login h1 a {
|
|
width: auto;
|
|
}
|
|
|
|
.interim-login #login_error,
|
|
.interim-login.login .message {
|
|
margin: 0 0 16px;
|
|
}
|
|
|
|
.interim-login.login form {
|
|
margin: 0;
|
|
}
|
|
|
|
/* Hide visually but not from screen readers */
|
|
.screen-reader-text,
|
|
.screen-reader-text span {
|
|
border: 0;
|
|
clip-path: inset(50%);
|
|
height: 1px;
|
|
margin: -1px;
|
|
overflow: hidden;
|
|
padding: 0;
|
|
position: absolute;
|
|
width: 1px;
|
|
/* Many screen reader and browser combinations announce broken words as they would appear visually. */
|
|
word-wrap: normal !important;
|
|
word-break: normal !important;
|
|
}
|
|
|
|
/* Hide the Edge "reveal password" native button */
|
|
input::-ms-reveal {
|
|
display: none;
|
|
}
|
|
|
|
#language-switcher {
|
|
padding: 0;
|
|
overflow: visible;
|
|
background: none;
|
|
border: none;
|
|
box-shadow: none;
|
|
}
|
|
|
|
#language-switcher select {
|
|
margin-left: 0.25em;
|
|
}
|
|
|
|
.language-switcher {
|
|
margin: 0 auto;
|
|
padding: 0 0 24px;
|
|
text-align: center;
|
|
}
|
|
|
|
.language-switcher form {
|
|
display: inline-block;
|
|
}
|
|
|
|
.language-switcher label {
|
|
display: block;
|
|
margin-bottom: 0.5em;
|
|
text-align: right;
|
|
}
|
|
|
|
.language-switcher label .dashicons {
|
|
margin-right: 0.5em;
|
|
width: auto;
|
|
height: auto;
|
|
}
|
|
|
|
.login .language-switcher .button {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
@media screen and (max-height: 550px) {
|
|
#login {
|
|
padding: 20px 0;
|
|
}
|
|
|
|
#language-switcher {
|
|
margin-top: 0;
|
|
}
|
|
}
|
|
|
|
|
|
@media screen and (max-width: 782px) {
|
|
.interim-login input[type=checkbox] {
|
|
width: 1rem;
|
|
height: 1rem;
|
|
}
|
|
|
|
.interim-login input[type=checkbox]:checked:before {
|
|
width: 1.3125rem;
|
|
height: 1.3125rem;
|
|
margin: -0.1875rem -0.25rem 0 0;
|
|
}
|
|
|
|
#language-switcher label,
|
|
#language-switcher select {
|
|
margin-left: 0;
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 400px) {
|
|
.login .language-switcher .button {
|
|
display: block;
|
|
margin: 5px auto 0;
|
|
}
|
|
}
|