First Commit
This commit is contained in:
1 parent
5eb9dd037d
commit
2e62c0e60b
11877 files changed
+2035312
No files matched your search
@@ -0,0 +1,239 @@
|
||||
<?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>
|
||||
Reference in new issue
Block a user