240 lines
11 KiB
PHP
240 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* "Pay with PayPal" button — production bridge to the PPCP checkout page
|
|
* ----------------------------------------------------------------------
|
|
* Distinct from the legacy paypal_checkout_button.php / paypal_form.php
|
|
* (the older Express Checkout on-page form). This file renders a single
|
|
* "Pay with PayPal" button; clicking it opens /sm/checkout.php, where the
|
|
* buyer reviews the order details and pays with any PayPal-supported
|
|
* method (PayPal, Venmo, Pay Later, Apple Pay, Google Pay — eligibility
|
|
* handled by the checkout widget).
|
|
*
|
|
* Deployment path: the integration folder is auto-detected under the
|
|
* document root (checks /pp-integration, then /sm — the production deploy
|
|
* is /pp-integration, e.g. https://dev.hotlunches.net/pp-integration/).
|
|
* Define PPP_WEB_BASE before this include to override.
|
|
*
|
|
* This file is include()d from payment_form.php inside the PayPal block,
|
|
* AFTER $balance, $trx_fee and $owing have been computed. It runs in that
|
|
* file's variable scope, so $school_record, $school_num, $user_num,
|
|
* $currencySymbol, / and $selected_amount are available.
|
|
*
|
|
* -- Gateway condition -------------------------------------------------
|
|
* A school's PayPal gateway can be "Express Checkout" (the current
|
|
* gateway — what every live school is on today) or "REST API Checkout".
|
|
* This button renders ONLY for "Express Checkout" schools — schools on
|
|
* "REST API Checkout" already have their own button and keep it. An
|
|
* unset/empty gateway value counts as Express Checkout, since that is
|
|
* the current gateway.
|
|
*
|
|
* -- How the money reaches the school ----------------------------------
|
|
* The school completes onboarding in the integration's onboard.php,
|
|
* which stores its
|
|
* PayPal merchant_id (keyed by the EXACT school name) in the `schools`
|
|
* table and data/school_merchants.json. order-api.php sets
|
|
* payee.merchant_id = <that merchant id> on every order it creates, so
|
|
* the capture settles directly into the school's own PayPal account.
|
|
* After capture, order-api.php also books the payment into the
|
|
* user_account ledger (idempotent on capture_id).
|
|
*
|
|
* The button only renders when the school has finished onboarding and
|
|
* PayPal reports it ready to receive payments — never a dead button.
|
|
*/
|
|
|
|
// Web path where the ppp/ integration is served. Auto-detected by probing the
|
|
// known deploy folders under the document root; define PPP_WEB_BASE before
|
|
// this include to override (e.g. if the folder is renamed again).
|
|
if (!defined('PPP_WEB_BASE')) {
|
|
$pppDocroot = rtrim($_SERVER['DOCUMENT_ROOT'] ?? '', '/');
|
|
foreach (['/pp-integration', '/sm'] as $pppCandidate) {
|
|
if (is_file($pppDocroot . $pppCandidate . '/checkout.php')) {
|
|
define('PPP_WEB_BASE', $pppCandidate);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Every hidden-return leaves an HTML comment in the page source saying WHY,
|
|
// so a missing button can be diagnosed with View Source instead of guesswork.
|
|
$pppWhy = function (string $reason): void {
|
|
echo "\n<!-- ppp-paypal-button hidden: " . htmlspecialchars($reason, ENT_QUOTES) . " -->\n";
|
|
};
|
|
|
|
// -- Guard: only render if the checkout page is actually deployed --------------
|
|
if (!defined('PPP_WEB_BASE')) {
|
|
$pppWhy('integration not found — checked ' . ($pppDocroot ?? '(no document root)')
|
|
. '{/pp-integration,/sm}/checkout.php');
|
|
return;
|
|
}
|
|
|
|
$pppFsBase = rtrim($_SERVER['DOCUMENT_ROOT'] ?? '', '/') . PPP_WEB_BASE;
|
|
$pppCheckoutFs = $pppFsBase . '/checkout.php';
|
|
if (!is_file($pppCheckoutFs)) {
|
|
$pppWhy('checkout.php missing at ' . $pppCheckoutFs);
|
|
return;
|
|
}
|
|
|
|
// -- Gateway condition: Express Checkout ONLY ----------------------------------
|
|
// "REST API Checkout" schools have their own existing button — never render
|
|
// this one for them. Unset/empty counts as Express (the current gateway).
|
|
// The real column on `schools` is paypal_checkout (verified against the live
|
|
// schema); the others are kept as fallbacks for older copies of the table.
|
|
$pppGateway = trim((string)($school_record['paypal_checkout']
|
|
?? $school_record['paypal_gateway']
|
|
?? $school_record['paypal_checkout_type'] ?? ''));
|
|
if ($pppGateway === '') $pppGateway = 'Express Checkout';
|
|
if ($pppGateway !== 'Express Checkout') {
|
|
$pppWhy('school gateway is "' . $pppGateway . '" — only Express Checkout schools get this button');
|
|
return;
|
|
}
|
|
|
|
// -- School must have completed PayPal onboarding and be ready -----------------
|
|
// School name MUST match the key used during onboarding exactly, or the
|
|
// merchant lookup in order-api.php will fail.
|
|
$pppSchoolName = trim($school_record['school_name'] ?? '');
|
|
if ($pppSchoolName === '') {
|
|
$pppWhy('school_record has no school_name');
|
|
return;
|
|
}
|
|
|
|
$pppMerchantsFile = $pppFsBase . '/data/school_merchants.json';
|
|
if (!is_file($pppMerchantsFile)) {
|
|
$pppWhy('merchants file missing at ' . $pppMerchantsFile);
|
|
return;
|
|
}
|
|
$pppMerchants = json_decode((string)file_get_contents($pppMerchantsFile), true) ?: [];
|
|
$pppRecord = $pppMerchants[$pppSchoolName] ?? null;
|
|
if (!$pppRecord || empty($pppRecord['merchant_id'])) {
|
|
$pppWhy('school "' . $pppSchoolName . '" not found in merchants file (keys: '
|
|
. implode(' | ', array_keys($pppMerchants)) . ') — onboarding name must match exactly');
|
|
return;
|
|
}
|
|
if (isset($pppRecord['paypal_ready']) && !$pppRecord['paypal_ready']) {
|
|
$pppWhy('school "' . $pppSchoolName . '" connected but paypal_ready=false — '
|
|
. 'PayPal has not confirmed it can receive payments yet');
|
|
return;
|
|
}
|
|
|
|
// -- Amounts -------------------------------------------------------------------
|
|
// payment_form.php has just computed:
|
|
// $balance — account balance owing (or the selected deposit amount)
|
|
// $trx_fee — the school's PayPal transaction fee
|
|
// $owing — $balance + $trx_fee ? this is what the buyer must be charged
|
|
$pppBase = isset($balance) ? (float)$balance : 0.0;
|
|
$pppFee = isset($trx_fee) ? (float)$trx_fee : 0.0;
|
|
$pppTotal = isset($owing) ? (float)$owing : $pppBase + $pppFee;
|
|
if ($pppTotal <= 0) {
|
|
$pppWhy('nothing to charge (total ' . number_format($pppTotal, 2) . ')');
|
|
return;
|
|
}
|
|
|
|
$pppIsDeposit = isset($selected_amount) && $selected_amount > 0;
|
|
|
|
$pppFeeTitle = trim($school_record['paypal_fee_title'] ?? '');
|
|
if ($pppFeeTitle === '' && function_exists('getLanguageText')) $pppFeeTitle = (string)getLanguageText('TEXT-TRX-FEE');
|
|
if ($pppFeeTitle === '') $pppFeeTitle = 'Transaction fee';
|
|
|
|
// Currency CODE (not symbol) — ISO 4217. Prefer the school's own
|
|
// paypal_currency column; then the PPP_CURRENCY constant; then USD.
|
|
$pppCurrency = strtoupper(trim((string)($school_record['paypal_currency'] ?? '')));
|
|
if (!preg_match('/^[A-Z]{3}$/', $pppCurrency)) {
|
|
$pppCurrency = defined('PPP_CURRENCY') ? PPP_CURRENCY : 'USD';
|
|
}
|
|
|
|
// -- Store the checkout context for /sm/checkout.php ---------------------------
|
|
// The session (name 'hotlunch') is shared with the checkout page, so amounts
|
|
// travel server-side and can't be tampered with through the URL.
|
|
$_SESSION['ppp_checkout'] = [
|
|
'school_num' => (int)($school_num ?? 0),
|
|
'school_name' => $pppSchoolName,
|
|
'user_num' => (int)($user_num ?? ($_SESSION['UserInfo']['record_num'] ?? 0)),
|
|
'buyer_email' => (string)($_SESSION['UserInfo']['email'] ?? ''),
|
|
'base_amount' => number_format($pppBase, 2, '.', ''),
|
|
'fee_amount' => number_format($pppFee, 2, '.', ''),
|
|
'total_amount' => number_format($pppTotal, 2, '.', ''),
|
|
'base_label' => $pppIsDeposit ? 'Deposit amount' : 'Account balance',
|
|
'fee_label' => $pppFeeTitle,
|
|
'currency' => $pppCurrency,
|
|
'currency_symbol' => (string)($currencySymbol ?? '$'),
|
|
'gateway' => $pppGateway,
|
|
'return_url' => (string)('/'),
|
|
'created_at' => time(),
|
|
];
|
|
|
|
$pppCheckoutUrl = PPP_WEB_BASE . '/checkout.php';
|
|
?>
|
|
<div class="ppp-pay-wrap">
|
|
<a href="<?php echo htmlspecialchars($pppCheckoutUrl); ?>" class="ppp-pay-btn">
|
|
<span class="ppp-pay-txt">Pay with</span>
|
|
<svg class="ppp-pay-logo" viewBox="0 0 100 26" xmlns="http://www.w3.org/2000/svg"
|
|
role="img" aria-label="PayPal">
|
|
<?php /* PayPal wordmark: dark-blue "Pay" + light-blue "Pal", drawn as one lockup. */ ?>
|
|
<g fill="#003087">
|
|
<path d="M11.9 1.2H4.6c-.5 0-.93.36-1 .86L.63 21.3c-.06.37.23.7.6.7h3.5c.5 0 .92-.36 1-.86l.8-5.08c.08-.5.5-.87 1-.87h2.31c4.81 0 7.59-2.33 8.32-6.94.33-2.02.01-3.6-.92-4.71C16.21 1.53 14.38 1.2 11.9 1.2zm.84 6.84c-.4 2.62-2.4 2.62-4.34 2.62h-1.1l.77-4.9c.05-.3.3-.51.6-.51h.51c1.32 0 2.56 0 3.21.75.38.45.5 1.12.35 2.04z"/>
|
|
<path d="M35.37 7.96h-3.48c-.3 0-.55.21-.6.51l-.15.97-.24-.35c-.76-1.1-2.45-1.47-4.13-1.47-3.87 0-7.17 2.93-7.81 7.04-.34 2.05.14 4.01 1.3 5.37 1.15 1.38 2.8 1.95 4.78 1.95 3.12 0 4.85-2 4.85-2l-.16.96c-.06.37.23.7.6.7h3.13c.5 0 .92-.37 1-.87l1.88-11.93c.06-.37-.23-.7-.6-.7zm-4.85 6.81c-.34 2-1.93 3.35-3.96 3.35-1.01 0-1.83-.33-2.35-.95-.52-.61-.71-1.48-.55-2.45.31-1.98 1.93-3.37 3.92-3.37.99 0 1.8.33 2.34.95.53.62.74 1.5.6 2.47z"/>
|
|
<path d="M53.9 7.96h-3.5c-.34 0-.65.17-.84.45l-4.82 7.1-2.04-6.82c-.13-.43-.52-.72-.97-.72h-3.44c-.42 0-.71.4-.58.79l3.85 11.29-3.62 5.1c-.28.4 0 .95.49.95h3.5c.33 0 .64-.16.83-.44L54.4 8.9c.28-.4 0-.94-.5-.94z"/>
|
|
</g>
|
|
<g fill="#0070e0">
|
|
<path d="M65.44 1.2h-7.3c-.5 0-.93.36-1 .86L54.17 21.3c-.06.37.23.7.6.7h3.75c.35 0 .65-.25.7-.6l.84-5.34c.08-.5.5-.87 1-.87h2.31c4.81 0 7.59-2.33 8.32-6.94.33-2.02.01-3.6-.92-4.71-1.03-1.21-2.85-1.54-5.33-1.54zm.84 6.84c-.4 2.62-2.4 2.62-4.34 2.62h-1.1l.78-4.9c.04-.3.3-.51.59-.51h.51c1.32 0 2.56 0 3.21.75.39.45.5 1.12.35 2.04z"/>
|
|
<path d="M88.91 7.96h-3.48c-.3 0-.55.21-.6.51l-.15.97-.25-.35c-.75-1.1-2.44-1.47-4.12-1.47-3.87 0-7.17 2.93-7.81 7.04-.34 2.05.14 4.01 1.3 5.37 1.15 1.38 2.8 1.95 4.78 1.95 3.12 0 4.85-2 4.85-2l-.16.96c-.06.37.23.7.6.7h3.13c.5 0 .92-.37 1-.87l1.88-11.93c.06-.37-.23-.7-.6-.7zm-4.85 6.81c-.34 2-1.93 3.35-3.96 3.35-1.01 0-1.83-.33-2.35-.95-.52-.61-.71-1.48-.55-2.45.31-1.98 1.93-3.37 3.92-3.37.99 0 1.8.33 2.34.95.53.62.74 1.5.6 2.47z"/>
|
|
<path d="M93.02 1.73L90 21.3c-.06.37.23.7.6.7h3c.5 0 .92-.37 1-.87L97.57 1.7c.06-.37-.23-.7-.6-.7h-3.35c-.3 0-.55.22-.6.52z"/>
|
|
</g>
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
|
|
<style>
|
|
.ppp-pay-wrap {
|
|
max-width: 340px;
|
|
margin: 0 auto;
|
|
text-align: center;
|
|
}
|
|
.ppp-pay-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
width: 100%;
|
|
min-height: 48px;
|
|
padding: 12px 24px;
|
|
box-sizing: border-box;
|
|
background: #ffc439;
|
|
border: none;
|
|
border-radius: 24px;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
line-height: 1;
|
|
color: #2c2e2f;
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
transition: background .15s ease, box-shadow .15s ease;
|
|
}
|
|
.ppp-pay-btn:hover,
|
|
.ppp-pay-btn:focus {
|
|
background: #f2ba36;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
|
|
text-decoration: none;
|
|
color: #2c2e2f;
|
|
}
|
|
.ppp-pay-btn:focus-visible {
|
|
outline: 2px solid #003087;
|
|
outline-offset: 2px;
|
|
}
|
|
.ppp-pay-btn:active {
|
|
background: #e5ad2c;
|
|
box-shadow: none;
|
|
}
|
|
.ppp-pay-txt {
|
|
/* Optical alignment: the wordmark's cap-height sits slightly high. */
|
|
transform: translateY(1px);
|
|
}
|
|
/* Height drives the lockup; width follows the 100:26 viewBox aspect ratio. */
|
|
.ppp-pay-logo {
|
|
height: 19px;
|
|
width: auto;
|
|
display: block;
|
|
flex: none;
|
|
}
|
|
</style>
|