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 @@
|
||||
# PayPal Merchant Onboarding Flow
|
||||
|
||||
## BEFORE FIX (The Problem) ❌
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 1. User clicks "Connect with PayPal" │
|
||||
│ → onboard.php creates referral │
|
||||
│ → Saves to pending_onboarding.json │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 2. Redirected to PayPal Onboarding │
|
||||
│ → User sees business account signup │
|
||||
│ → User CLOSES browser WITHOUT completing ❌ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 3. Returns to return.php │
|
||||
│ → Queries PayPal API for merchant_id │
|
||||
│ → PayPal returns: "NEG5HMUKCTQNL" (PARTNER ID!) ❌ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 4. OLD CODE: Accepts partner ID as merchant ID ❌ │
|
||||
│ → Saves "NEG5HMUKCTQNL" to merchants.json │
|
||||
│ → WRONG! This is YOUR ID, not the merchant's! │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
STUCK FOREVER 😞
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AFTER FIX (The Solution) ✅
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 1. User clicks "Connect with PayPal" │
|
||||
│ → onboard.php creates referral │
|
||||
│ → Saves to pending_onboarding.json │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 2. Redirected to PayPal Onboarding │
|
||||
│ → User sees business account signup │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↙ ↘
|
||||
SCENARIO A SCENARIO B
|
||||
(Incomplete) (Complete)
|
||||
|
||||
┌──────────────────────────┐ ┌──────────────────────────┐
|
||||
│ User CLOSES browser │ │ User COMPLETES all │
|
||||
│ WITHOUT finishing ❌ │ │ steps at PayPal ✅ │
|
||||
└──────────────────────────┘ └──────────────────────────┘
|
||||
↓ ↓
|
||||
┌──────────────────────────┐ ┌──────────────────────────┐
|
||||
│ Returns to return.php │ │ Returns to return.php │
|
||||
│ PayPal returns: │ │ PayPal returns: │
|
||||
│ "NEG5HMUKCTQNL" │ │ "ABC123XYZ456" ✅ │
|
||||
│ (partner ID) │ │ (real merchant ID) │
|
||||
└──────────────────────────┘ └──────────────────────────┘
|
||||
↓ ↓
|
||||
┌──────────────────────────┐ ┌──────────────────────────┐
|
||||
│ NEW CODE: │ │ NEW CODE: │
|
||||
│ ✅ REJECTS partner ID │ │ ✅ ACCEPTS merchant ID │
|
||||
│ ✅ Marks as incomplete │ │ ✅ Saves to merchants │
|
||||
│ ✅ Shows warning │ │ ✅ Shows success │
|
||||
└──────────────────────────┘ └──────────────────────────┘
|
||||
↓ ↓
|
||||
┌──────────────────────────┐ ┌──────────────────────────┐
|
||||
│ Dashboard shows: │ │ Dashboard shows: │
|
||||
│ ⚠️ Incomplete │ │ ✅ Connected │
|
||||
│ "Complete PayPal │ │ "Ready to accept │
|
||||
│ signup to continue" │ │ payments" │
|
||||
└──────────────────────────┘ └──────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Structure Comparison
|
||||
|
||||
### BEFORE FIX ❌
|
||||
```json
|
||||
// pending_onboarding.json
|
||||
{
|
||||
"school_ab32258ab0": {
|
||||
"school": "m",
|
||||
"created_at": 1760884706,
|
||||
"partner_referral_id": "YzU2NGVl...",
|
||||
"referral_token": "YzU2NGVl...",
|
||||
"merchant_id": "NEG5HMUKCTQNL", ← WRONG! This is partner ID
|
||||
"status": "connected" ← FALSE! Not really connected
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AFTER FIX - Incomplete ⚠️
|
||||
```json
|
||||
// pending_onboarding.json
|
||||
{
|
||||
"school_ab32258ab0": {
|
||||
"school": "m",
|
||||
"created_at": 1760884706,
|
||||
"partner_referral_id": "YzU2NGVl...",
|
||||
"referral_token": "YzU2NGVl...",
|
||||
"status": "incomplete_onboarding", ← Honest status
|
||||
"note": "Merchant did not complete PayPal onboarding",
|
||||
"last_return_at": 1760885000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AFTER FIX - Complete ✅
|
||||
```json
|
||||
// pending_onboarding.json
|
||||
{
|
||||
"school_3387003d82": {
|
||||
"school": "b",
|
||||
"created_at": 1760797314,
|
||||
"partner_referral_id": "MTQ2Njlm...",
|
||||
"referral_token": "MTQ2Njlm...",
|
||||
"merchant_id": "796GQ4VM8SWSQ", ← CORRECT! Different ID
|
||||
"status": "connected", ← TRUE! Really connected
|
||||
"connected_at": 1760797400
|
||||
}
|
||||
}
|
||||
|
||||
// merchants.json
|
||||
{
|
||||
"b": {
|
||||
"merchant_id": "796GQ4VM8SWSQ",
|
||||
"tracking_id": "school_3387003d82",
|
||||
"connected_at": 1760797400
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Changes in Code
|
||||
|
||||
### return.php
|
||||
```php
|
||||
// OLD CODE ❌
|
||||
if (!empty($resp['body']['merchant_id'])) {
|
||||
$merchantId = $resp['body']['merchant_id']; // Accepts ANY ID
|
||||
}
|
||||
|
||||
// NEW CODE ✅
|
||||
if (!empty($resp['body']['merchant_id'])) {
|
||||
$foundId = $resp['body']['merchant_id'];
|
||||
// CRITICAL: Don't accept partner's own ID
|
||||
if (strcasecmp($foundId, $config['partner_merchant_id']) !== 0) {
|
||||
$merchantId = $foundId; // Only accept if different
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### sync_merchants.php
|
||||
```php
|
||||
// OLD CODE ❌
|
||||
if (!empty($resp['body']['merchant_id'])) {
|
||||
$merchantId = $resp['body']['merchant_id']; // Blindly accepts
|
||||
}
|
||||
|
||||
// NEW CODE ✅
|
||||
if (!empty($resp['body']['merchant_id'])) {
|
||||
$foundId = $resp['body']['merchant_id'];
|
||||
// Don't accept partner ID - means onboarding not completed
|
||||
if (strcasecmp($foundId, $config['partner_merchant_id']) !== 0) {
|
||||
$merchantId = $foundId;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
### Test 1: Incomplete Onboarding ⚠️
|
||||
1. Click "Connect with PayPal"
|
||||
2. See PayPal signup page
|
||||
3. **Immediately click "Return to School Lunch App"**
|
||||
4. **Expected Result**:
|
||||
- ⚠️ "Onboarding Incomplete" warning
|
||||
- Status: `incomplete_onboarding`
|
||||
- NO merchant_id saved
|
||||
|
||||
### Test 2: Complete Onboarding ✅
|
||||
1. Click "Connect with PayPal"
|
||||
2. See PayPal signup page
|
||||
3. **Complete ALL steps** (business info, email verification, etc.)
|
||||
4. Click "Return to Application"
|
||||
5. **Expected Result**:
|
||||
- ✅ "Onboarding completed" success message
|
||||
- Status: `connected`
|
||||
- Merchant ID saved (NOT `NEG5HMUKCTQNL`)
|
||||
|
||||
### Test 3: Sync Pending ⏳
|
||||
1. Complete onboarding but merchant_id not immediately available
|
||||
2. Click "🔄 Sync Now"
|
||||
3. **Expected Result**:
|
||||
- System queries PayPal API
|
||||
- Retrieves merchant_id
|
||||
- Updates status to `connected`
|
||||
|
||||
---
|
||||
|
||||
## Success Indicators ✅
|
||||
|
||||
You'll know it's working when:
|
||||
|
||||
1. **Different Merchant IDs**: Each merchant gets unique ID (not `NEG5HMUKCTQNL`)
|
||||
2. **Clear Status**: See `connected` or `incomplete_onboarding` (not stuck)
|
||||
3. **Warning Messages**: Incomplete onboardings show warnings
|
||||
4. **Real Payments**: Can create orders with actual merchant IDs
|
||||
|
||||
---
|
||||
|
||||
## Your Real Example 🎯
|
||||
|
||||
From your `sync_debug.json`, school "b" worked correctly:
|
||||
|
||||
```json
|
||||
{
|
||||
"ts": 1760884845,
|
||||
"track": "school_3387003d82",
|
||||
"school": "b",
|
||||
"attempt": "by-tracking",
|
||||
"status": 200,
|
||||
"body": {
|
||||
"merchant_id": "796GQ4VM8SWSQ", ← SUCCESS! ✅
|
||||
"tracking_id": "school_3387003d82"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is what ALL your merchants should look like! 🎉
|
||||
@@ -0,0 +1,286 @@
|
||||
# 🚀 Production Deployment Checklist
|
||||
|
||||
## Before Going Live with PayPal Merchant Onboarding
|
||||
|
||||
---
|
||||
|
||||
## ✅ Configuration
|
||||
|
||||
- [ ] **Update `config.php` with LIVE credentials**
|
||||
```php
|
||||
'env' => 'live', // Change from 'sandbox'
|
||||
'client_id' => 'YOUR_LIVE_CLIENT_ID',
|
||||
'client_secret' => 'YOUR_LIVE_CLIENT_SECRET',
|
||||
'partner_merchant_id' => 'YOUR_LIVE_PARTNER_ID',
|
||||
```
|
||||
|
||||
- [ ] **Verify partner merchant ID**
|
||||
- Go to: https://www.paypal.com/businessprofile/settings
|
||||
- Account IDs section
|
||||
- Copy your Merchant ID (Payer ID)
|
||||
- Paste into `config.php`
|
||||
|
||||
- [ ] **Update base_url**
|
||||
```php
|
||||
'base_url' => 'https://yourdomain.com/payp', // Your actual domain
|
||||
```
|
||||
|
||||
- [ ] **Enable HTTPS**
|
||||
- PayPal requires HTTPS in production
|
||||
- Install SSL certificate
|
||||
- Force HTTPS in Apache/Nginx
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security
|
||||
|
||||
- [ ] **Secure storage directory**
|
||||
```apache
|
||||
# Add to .htaccess in /data folder
|
||||
Deny from all
|
||||
```
|
||||
|
||||
- [ ] **File permissions**
|
||||
```bash
|
||||
chmod 700 data/
|
||||
chmod 600 data/*.json
|
||||
```
|
||||
|
||||
- [ ] **Environment variables (recommended)**
|
||||
- Move sensitive config to environment variables
|
||||
- Don't commit credentials to Git
|
||||
|
||||
- [ ] **Add CSRF protection**
|
||||
- Add tokens to forms
|
||||
- Validate on submission
|
||||
|
||||
- [ ] **Rate limiting**
|
||||
- Limit onboarding requests per IP
|
||||
- Prevent abuse
|
||||
|
||||
---
|
||||
|
||||
## 📝 User Communication
|
||||
|
||||
- [ ] **Onboarding instructions page**
|
||||
- Clear step-by-step guide
|
||||
- Screenshots of PayPal flow
|
||||
- What to expect at each step
|
||||
- Troubleshooting tips
|
||||
|
||||
- [ ] **Email templates**
|
||||
- Welcome email after successful connection
|
||||
- Reminder email for incomplete onboarding
|
||||
- Support contact information
|
||||
|
||||
- [ ] **Help documentation**
|
||||
- FAQ about merchant onboarding
|
||||
- Common issues and solutions
|
||||
- How to reconnect if needed
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
- [ ] **Test with LIVE PayPal sandbox first**
|
||||
- Use PayPal sandbox with production code
|
||||
- Verify all flows work
|
||||
|
||||
- [ ] **Test complete onboarding**
|
||||
- Full business account creation
|
||||
- Email verification
|
||||
- Bank account linking (if required)
|
||||
- Return to your app
|
||||
|
||||
- [ ] **Test incomplete onboarding**
|
||||
- Verify warning messages show
|
||||
- Verify no bad data is saved
|
||||
- Test retry flow
|
||||
|
||||
- [ ] **Test sync functionality**
|
||||
- Manually trigger sync
|
||||
- Verify it finds completed merchants
|
||||
- Verify it marks incomplete ones
|
||||
|
||||
- [ ] **Test payment creation**
|
||||
- Create orders with connected merchants
|
||||
- Verify proper merchant_id routing
|
||||
- Test capture flow
|
||||
|
||||
---
|
||||
|
||||
## 📊 Monitoring
|
||||
|
||||
- [ ] **Log monitoring**
|
||||
- Set up alerts for errors
|
||||
- Monitor `return_debug.json`
|
||||
- Monitor `sync_debug.json`
|
||||
|
||||
- [ ] **Merchant status tracking**
|
||||
- Dashboard for admin to view all merchants
|
||||
- Filter by status (connected, incomplete, pending)
|
||||
- Export functionality
|
||||
|
||||
- [ ] **Automated sync**
|
||||
- Cron job to run sync_merchants.php
|
||||
- Schedule: Every 5-10 minutes
|
||||
- Alert on repeated failures
|
||||
|
||||
- [ ] **Metrics tracking**
|
||||
- Onboarding completion rate
|
||||
- Average time to complete
|
||||
- Common drop-off points
|
||||
- Failed onboarding reasons
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Ongoing Maintenance
|
||||
|
||||
- [ ] **Weekly review**
|
||||
- Check incomplete onboardings
|
||||
- Follow up with merchants
|
||||
- Resolve issues
|
||||
|
||||
- [ ] **Monthly cleanup**
|
||||
- Archive old completed entries
|
||||
- Remove abandoned onboardings (>30 days old)
|
||||
- Backup data files
|
||||
|
||||
- [ ] **Quarterly updates**
|
||||
- Check for PayPal API changes
|
||||
- Update partner integration if needed
|
||||
- Review and update documentation
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Support Process
|
||||
|
||||
- [ ] **Support email/form**
|
||||
- Dedicated support for onboarding issues
|
||||
- Quick response time (<24 hours)
|
||||
|
||||
- [ ] **Troubleshooting guide for support team**
|
||||
- How to check merchant status
|
||||
- How to trigger manual sync
|
||||
- How to view debug logs
|
||||
- When to escalate to PayPal
|
||||
|
||||
- [ ] **Partner support contact**
|
||||
- PayPal Partner Support phone/email
|
||||
- Your Partner Manager contact
|
||||
- Escalation process
|
||||
|
||||
---
|
||||
|
||||
## 📋 Pre-Launch Test Cases
|
||||
|
||||
### Test Case 1: Happy Path ✅
|
||||
1. Merchant clicks "Connect with PayPal"
|
||||
2. Completes entire onboarding at PayPal
|
||||
3. Returns to your app
|
||||
4. Sees success message
|
||||
5. Merchant ID saved correctly
|
||||
6. Can create and capture payments
|
||||
|
||||
### Test Case 2: Incomplete Onboarding ⚠️
|
||||
1. Merchant clicks "Connect with PayPal"
|
||||
2. Closes browser without completing
|
||||
3. Returns to your app (manually)
|
||||
4. Sees warning message
|
||||
5. NO merchant ID saved
|
||||
6. Can retry onboarding
|
||||
|
||||
### Test Case 3: Delayed Propagation ⏳
|
||||
1. Merchant completes onboarding
|
||||
2. Returns to your app
|
||||
3. Merchant ID not immediately available
|
||||
4. Clicks "Sync Now"
|
||||
5. System retrieves merchant ID
|
||||
6. Status updated to connected
|
||||
|
||||
### Test Case 4: Multiple Merchants 👥
|
||||
1. Connect merchant A (complete)
|
||||
2. Connect merchant B (incomplete)
|
||||
3. Connect merchant C (complete)
|
||||
4. Dashboard shows correct status for each
|
||||
5. Can create orders ONLY for A and C
|
||||
|
||||
### Test Case 5: Reconnection 🔄
|
||||
1. Merchant was incomplete
|
||||
2. Clicks "Connect with PayPal" again
|
||||
3. Completes full onboarding
|
||||
4. Returns successfully
|
||||
5. Status updated from incomplete to connected
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Production Readiness Checklist
|
||||
|
||||
### Code Quality
|
||||
- [ ] Error handling for all API calls
|
||||
- [ ] Input validation on all forms
|
||||
- [ ] SQL injection protection (if using DB)
|
||||
- [ ] XSS protection
|
||||
- [ ] Proper logging (not echo/print_r)
|
||||
|
||||
### Performance
|
||||
- [ ] Response times < 2 seconds
|
||||
- [ ] Optimize database queries (if using DB)
|
||||
- [ ] Cache frequently accessed data
|
||||
- [ ] CDN for static assets
|
||||
|
||||
### Scalability
|
||||
- [ ] Can handle 100+ merchants
|
||||
- [ ] Concurrent onboarding requests
|
||||
- [ ] File storage vs. database decision
|
||||
- [ ] Backup strategy
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Launch Day
|
||||
|
||||
- [ ] Deploy to production
|
||||
- [ ] Verify configuration
|
||||
- [ ] Test one complete onboarding
|
||||
- [ ] Monitor logs for 24 hours
|
||||
- [ ] Have support team ready
|
||||
- [ ] Gradual rollout (if possible)
|
||||
|
||||
---
|
||||
|
||||
## 📞 Emergency Contacts
|
||||
|
||||
- [ ] PayPal Partner Support: _________________
|
||||
- [ ] PayPal Technical Issues: _________________
|
||||
- [ ] Your DevOps Team: _________________
|
||||
- [ ] Your Manager: _________________
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Post-Launch (First Week)
|
||||
|
||||
- [ ] Day 1: Monitor every hour
|
||||
- [ ] Day 2-3: Monitor every 4 hours
|
||||
- [ ] Day 4-7: Monitor twice daily
|
||||
- [ ] Review all incomplete onboardings
|
||||
- [ ] Gather user feedback
|
||||
- [ ] Document any issues found
|
||||
- [ ] Plan improvements
|
||||
|
||||
---
|
||||
|
||||
## 📌 Remember
|
||||
|
||||
> The fix ensures that incomplete onboardings are properly detected and handled. Your main job now is to help merchants complete the full PayPal business account setup process. Communication is key! 🗝️
|
||||
|
||||
---
|
||||
|
||||
## Need Help?
|
||||
|
||||
Refer back to:
|
||||
- `SOLUTION_SUMMARY.md` - Understanding the fix
|
||||
- `FLOW_DIAGRAM.md` - Visual representation
|
||||
- `README_ONBOARDING_FIX.md` - Detailed technical docs
|
||||
- `QUICK_START.md` - Testing guide
|
||||
|
||||
Good luck with your launch! 🚀
|
||||
@@ -0,0 +1,168 @@
|
||||
# 🚀 QUICK START - Fix Your Merchant Onboarding
|
||||
|
||||
## What Just Happened?
|
||||
|
||||
I found and fixed the bug causing your merchants to get stuck in "onboarding" state. The problem: **your code was accepting your own partner ID as if it were the merchant's ID when they didn't complete PayPal signup.**
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Step 1: Clean Up Old Data (2 minutes)
|
||||
|
||||
Open your browser and go to:
|
||||
```
|
||||
http://localhost/payp/cleanup.php
|
||||
```
|
||||
|
||||
This will:
|
||||
- Remove incorrect merchant IDs (your partner ID saved as merchant IDs)
|
||||
- Mark incomplete onboardings properly
|
||||
- Show you what was cleaned
|
||||
|
||||
**Do this NOW before testing!**
|
||||
|
||||
---
|
||||
|
||||
## ✅ Step 2: Test Complete Onboarding (5 minutes)
|
||||
|
||||
1. Go to: `http://localhost/payp/`
|
||||
|
||||
2. Fill in a school name and click **"Connect with PayPal"**
|
||||
|
||||
3. **IMPORTANT**: When redirected to PayPal:
|
||||
- ✅ **DO**: Complete the ENTIRE business account setup
|
||||
- ✅ **DO**: Fill in all required fields
|
||||
- ✅ **DO**: Click through all steps until PayPal shows "Return to Application"
|
||||
- ❌ **DON'T**: Close the browser early
|
||||
- ❌ **DON'T**: Skip steps
|
||||
|
||||
4. Click **"Return to School Lunch App"** when PayPal prompts
|
||||
|
||||
5. **Verify Success**:
|
||||
- You should see: ✅ "Onboarding completed and merchant saved"
|
||||
- Dashboard shows the school in "Connected Schools"
|
||||
- Merchant ID is **NOT** `NEG5HMUKCTQNL`
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Step 3: Test Incomplete Onboarding (2 minutes)
|
||||
|
||||
1. Go to: `http://localhost/payp/`
|
||||
|
||||
2. Fill in a different school name and click **"Connect with PayPal"**
|
||||
|
||||
3. **As soon as PayPal page loads**:
|
||||
- Click "Return to School Lunch App" WITHOUT completing signup
|
||||
- OR just close the browser tab
|
||||
|
||||
4. **Verify Failure Handling**:
|
||||
- You should see: ⚠️ "Onboarding Incomplete" warning
|
||||
- Dashboard shows the school in "Pending Onboarding" with warning icon
|
||||
- NO merchant ID is saved
|
||||
- Status shows: `incomplete_onboarding`
|
||||
|
||||
---
|
||||
|
||||
## 📊 Step 4: Check Your Data
|
||||
|
||||
Open these files and verify:
|
||||
|
||||
### `data/merchants.json`
|
||||
Should contain only COMPLETED merchants with DIFFERENT IDs:
|
||||
```json
|
||||
{
|
||||
"Test School": {
|
||||
"merchant_id": "SOME_UNIQUE_ID", // NOT NEG5HMUKCTQNL
|
||||
"tracking_id": "school_xxxxx",
|
||||
"connected_at": 1234567890
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `data/pending_onboarding.json`
|
||||
Should show clear status for each:
|
||||
```json
|
||||
{
|
||||
"school_xxxxx": {
|
||||
"school": "Complete School",
|
||||
"merchant_id": "SOME_UNIQUE_ID",
|
||||
"status": "connected" // ✅
|
||||
},
|
||||
"school_yyyyy": {
|
||||
"school": "Incomplete School",
|
||||
"status": "incomplete_onboarding", // ⚠️
|
||||
"note": "Merchant did not complete PayPal onboarding"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Expected Results
|
||||
|
||||
### ✅ SUCCESS - Complete Onboarding
|
||||
- Dashboard: ✅ Green success message
|
||||
- Merchant ID: Different from `NEG5HMUKCTQNL`
|
||||
- Status: `connected`
|
||||
- Can create orders
|
||||
|
||||
### ⚠️ HANDLED - Incomplete Onboarding
|
||||
- Dashboard: ⚠️ Yellow warning message
|
||||
- No merchant ID saved
|
||||
- Status: `incomplete_onboarding`
|
||||
- Clear instructions to complete signup
|
||||
|
||||
### ❌ OLD BUG (Now Fixed)
|
||||
- ~~Saves `NEG5HMUKCTQNL` as merchant ID~~
|
||||
- ~~Status stuck at `connected` but not really connected~~
|
||||
- ~~No warning messages~~
|
||||
- ~~Can't create orders~~
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Problem: Still seeing `NEG5HMUKCTQNL` as merchant ID
|
||||
- **Solution**: Run `cleanup.php` again, then test with a NEW school
|
||||
|
||||
### Problem: Dashboard doesn't show warnings
|
||||
- **Solution**: Clear browser cache and refresh `index.php`
|
||||
|
||||
### Problem: Can't complete PayPal onboarding in sandbox
|
||||
- **Solution**: Make sure you're using a PayPal sandbox business account for testing
|
||||
- Check: https://developer.paypal.com/developer/accounts/
|
||||
|
||||
### Problem: Getting errors when creating orders
|
||||
- **Solution**: Verify the merchant_id in `merchants.json` is NOT your partner ID
|
||||
|
||||
---
|
||||
|
||||
## 📋 Quick Checklist
|
||||
|
||||
- [ ] Ran `cleanup.php` to fix old data
|
||||
- [ ] Tested complete onboarding (finish all PayPal steps)
|
||||
- [ ] Saw ✅ success message
|
||||
- [ ] Verified merchant ID is different from `NEG5HMUKCTQNL`
|
||||
- [ ] Tested incomplete onboarding (close browser early)
|
||||
- [ ] Saw ⚠️ warning message
|
||||
- [ ] Verified NO merchant ID saved for incomplete
|
||||
- [ ] Checked `data/merchants.json` has correct IDs
|
||||
- [ ] Checked `data/pending_onboarding.json` has correct statuses
|
||||
|
||||
---
|
||||
|
||||
## 📚 More Information
|
||||
|
||||
- **Full explanation**: Read `SOLUTION_SUMMARY.md`
|
||||
- **Visual flow**: Read `FLOW_DIAGRAM.md`
|
||||
- **Detailed docs**: Read `README_ONBOARDING_FIX.md`
|
||||
- **Diagnostics**: Open `partner_diag.php` in browser
|
||||
|
||||
---
|
||||
|
||||
## 🎉 You're Done!
|
||||
|
||||
Your PayPal merchant onboarding is now fixed! The key takeaway:
|
||||
|
||||
> **Merchants MUST complete the entire PayPal business account signup for you to receive their merchant ID. If they don't finish, the system now correctly detects this and shows a warning instead of saving incorrect data.**
|
||||
|
||||
Questions? Check the documentation files created in your project. 🚀
|
||||
@@ -0,0 +1,145 @@
|
||||
# PayPal Merchant Onboarding - Issue Fixed
|
||||
|
||||
## The Problem You Were Experiencing
|
||||
|
||||
Your application was getting **stuck in "onboarding" state** and never receiving the actual merchant ID. Instead, you were seeing your **partner merchant ID** (`NEG5HMUKCTQNL`) returned for all merchants.
|
||||
|
||||
### Root Cause
|
||||
|
||||
When merchants click the "Connect with PayPal" button, they are redirected to PayPal's onboarding flow. However:
|
||||
|
||||
1. **If they don't complete the entire signup process** (close browser, navigate away, etc.)
|
||||
2. PayPal's API returns your **partner's own merchant ID** instead of the new merchant's ID
|
||||
3. Your code was incorrectly saving this partner ID as if it were the merchant's ID
|
||||
|
||||
## What Was Fixed
|
||||
|
||||
### 1. **Partner ID Detection**
|
||||
Added checks to reject the partner's own ID (`NEG5HMUKCTQNL`) when it's returned:
|
||||
|
||||
```php
|
||||
// Don't accept the partner's own ID as merchant ID
|
||||
if (strcasecmp($foundId, $config['partner_merchant_id']) !== 0) {
|
||||
$merchantId = $foundId;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **Better Status Tracking**
|
||||
Now tracks onboarding status:
|
||||
- `incomplete_onboarding` - Merchant started but didn't complete PayPal signup
|
||||
- `connected` - Merchant completed signup and ID is available
|
||||
- `pending` - Waiting for PayPal to propagate the data
|
||||
|
||||
### 3. **User Feedback**
|
||||
Enhanced the UI to show:
|
||||
- ⚠️ Warning when onboarding is incomplete
|
||||
- Clear instructions to complete the PayPal signup process
|
||||
- Sync button to check for completed onboardings
|
||||
|
||||
## How To Use It Properly
|
||||
|
||||
### For Testing (Sandbox):
|
||||
|
||||
1. **Start Onboarding**: Click "Connect with PayPal" and enter a school name
|
||||
2. **Complete the FULL PayPal Flow**:
|
||||
- You'll be redirected to PayPal's sandbox signup
|
||||
- **IMPORTANT**: You must complete the entire business account creation
|
||||
- Don't just close the browser - finish all steps at PayPal
|
||||
3. **Return**: After completing, you'll be redirected back
|
||||
4. **Verify**: Check that the merchant ID is saved (not `NEG5HMUKCTQNL`)
|
||||
|
||||
### What Success Looks Like:
|
||||
|
||||
In `data/pending_onboarding.json`, you should see:
|
||||
```json
|
||||
{
|
||||
"school_xxx": {
|
||||
"school": "Test School",
|
||||
"merchant_id": "SOME_OTHER_ID_NOT_YOUR_PARTNER_ID",
|
||||
"status": "connected",
|
||||
"connected_at": 1234567890
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### What Failure Looks Like (Before Fix):
|
||||
|
||||
```json
|
||||
{
|
||||
"school_xxx": {
|
||||
"school": "Test School",
|
||||
"merchant_id": "NEG5HMUKCTQNL", // ❌ This is YOUR partner ID, not the merchant's!
|
||||
"status": "connected"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### What Incomplete Looks Like (After Fix):
|
||||
|
||||
```json
|
||||
{
|
||||
"school_xxx": {
|
||||
"school": "Test School",
|
||||
"status": "incomplete_onboarding",
|
||||
"note": "Merchant did not complete PayPal onboarding. Partner ID was returned instead."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. **return.php** - Return handler after PayPal onboarding
|
||||
- Added partner ID detection and rejection
|
||||
- Added incomplete status tracking
|
||||
|
||||
2. **sync_merchants.php** - Sync pending merchants
|
||||
- Added partner ID filtering in all lookup methods
|
||||
- Marks incomplete onboardings
|
||||
|
||||
3. **index.php** - Main dashboard
|
||||
- Shows incomplete onboarding warnings
|
||||
- Better status messages
|
||||
|
||||
## Testing Your Fix
|
||||
|
||||
1. **Clear old data**:
|
||||
- Delete or backup `data/merchants.json` and `data/pending_onboarding.json`
|
||||
|
||||
2. **Test incomplete onboarding**:
|
||||
- Click "Connect with PayPal"
|
||||
- When redirected to PayPal, immediately click "Return to School Lunch App" WITHOUT completing signup
|
||||
- You should see "⚠️ Onboarding Incomplete" warning
|
||||
- No incorrect merchant ID should be saved
|
||||
|
||||
3. **Test complete onboarding**:
|
||||
- Click "Connect with PayPal"
|
||||
- **Complete the ENTIRE PayPal business account setup**
|
||||
- You should see "✅ Onboarding completed"
|
||||
- Verify the merchant ID is NOT `NEG5HMUKCTQNL`
|
||||
|
||||
## Expected Merchant IDs in Sandbox
|
||||
|
||||
- ✅ **Different from your partner ID**: Good! This is a real merchant
|
||||
- ❌ **Same as partner ID** (`NEG5HMUKCTQNL`): Bad! Onboarding not completed
|
||||
|
||||
## One Successful Example Found
|
||||
|
||||
Looking at your debug logs, I found ONE successful onboarding:
|
||||
- School: "b"
|
||||
- Tracking ID: `school_3387003d82`
|
||||
- Merchant ID: `796GQ4VM8SWSQ` ✅
|
||||
|
||||
This proves your integration works when merchants **complete the full signup process**.
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Test with a fresh onboarding
|
||||
2. Make sure to complete ALL steps at PayPal during signup
|
||||
3. For production, ensure your merchants understand they must complete the entire PayPal business account setup
|
||||
4. Monitor `data/pending_onboarding.json` for `incomplete_onboarding` status entries
|
||||
|
||||
## Questions?
|
||||
|
||||
- Check `partner_diag.php` to see all merchant integrations
|
||||
- Review `data/sync_debug.json` for detailed API responses
|
||||
- Ensure your PayPal Partner credentials are correct in `config.php`
|
||||
@@ -0,0 +1,178 @@
|
||||
# 🎯 SOLUTION SUMMARY - PayPal Merchant Onboarding Issue
|
||||
|
||||
## The Problem
|
||||
You were **always** getting your business account in "onboarding" state and never receiving actual merchant IDs. All entries in `pending_onboarding.json` showed only:
|
||||
- `partner_referral_id`
|
||||
- `referral_token`
|
||||
- **NO `merchant_id`** field
|
||||
|
||||
When checking, you kept finding **your own partner merchant ID** (`NEG5HMUKCTQNL`) instead of unique merchant IDs.
|
||||
|
||||
---
|
||||
|
||||
## Root Cause Discovered ✅
|
||||
|
||||
After analyzing your debug logs, I found the issue:
|
||||
|
||||
**PayPal returns YOUR PARTNER ID when merchants DON'T complete the full signup process!**
|
||||
|
||||
Looking at `data/return_debug.json` and `data/sync_debug.json`:
|
||||
- Most entries show: `"merchant_id": "NEG5HMUKCTQNL"` ❌ (your partner ID)
|
||||
- ONE successful entry: `"merchant_id": "796GQ4VM8SWSQ"` ✅ (actual merchant - school "b")
|
||||
|
||||
This proves:
|
||||
1. Your integration code is **working correctly**
|
||||
2. Merchants are **not completing** the PayPal onboarding flow
|
||||
3. When incomplete, PayPal returns the partner's ID as a placeholder
|
||||
|
||||
---
|
||||
|
||||
## What I Fixed 🔧
|
||||
|
||||
### 1. **Partner ID Detection**
|
||||
Modified these files to reject your partner ID:
|
||||
- `return.php` - Return handler after onboarding
|
||||
- `sync_merchants.php` - Merchant sync utility
|
||||
|
||||
Now the code checks:
|
||||
```php
|
||||
if (strcasecmp($foundId, $config['partner_merchant_id']) !== 0) {
|
||||
// Only accept if NOT the partner's own ID
|
||||
$merchantId = $foundId;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **Status Tracking**
|
||||
Added proper status field:
|
||||
- `incomplete_onboarding` - User didn't finish PayPal signup
|
||||
- `connected` - Successfully onboarded with real merchant ID
|
||||
- `pending` - Waiting for PayPal to propagate
|
||||
|
||||
### 3. **User Interface**
|
||||
Enhanced `index.php` to show:
|
||||
- ⚠️ **Warning** for incomplete onboardings
|
||||
- Clear instructions for users
|
||||
- Visual status indicators
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `return.php` | Added partner ID filtering, status tracking |
|
||||
| `sync_merchants.php` | Added partner ID checks in all lookup methods |
|
||||
| `index.php` | Added incomplete onboarding warnings & better UI |
|
||||
| `cleanup.php` | **NEW** - Utility to remove incorrect data |
|
||||
| `README_ONBOARDING_FIX.md` | **NEW** - Full documentation |
|
||||
|
||||
---
|
||||
|
||||
## How To Fix Your Data 🧹
|
||||
|
||||
### Step 1: Run Cleanup
|
||||
```
|
||||
http://localhost/payp/cleanup.php
|
||||
```
|
||||
This will:
|
||||
- Remove entries with incorrect partner IDs
|
||||
- Mark incomplete onboardings
|
||||
- Show you what was cleaned
|
||||
|
||||
### Step 2: Test New Onboarding
|
||||
1. Go to `http://localhost/payp/`
|
||||
2. Click "Connect with PayPal"
|
||||
3. **IMPORTANT**: Complete the ENTIRE PayPal business account setup
|
||||
- Don't just close the browser
|
||||
- Finish all signup steps at PayPal
|
||||
4. Click "Return to Application" when done
|
||||
5. Verify you see: ✅ "Onboarding completed and merchant saved"
|
||||
|
||||
### Step 3: Verify Success
|
||||
Check `data/pending_onboarding.json`:
|
||||
```json
|
||||
{
|
||||
"school_xxx": {
|
||||
"school": "Your School",
|
||||
"merchant_id": "SOME_NEW_ID", // NOT NEG5HMUKCTQNL
|
||||
"status": "connected"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why This Was Happening 🤔
|
||||
|
||||
### In PayPal Sandbox Testing:
|
||||
1. You click "Connect with PayPal"
|
||||
2. Redirected to PayPal's onboarding page
|
||||
3. **If you don't complete all steps** and just click "Return"
|
||||
4. PayPal returns your partner ID as a fallback
|
||||
5. Old code accepted it and saved it incorrectly
|
||||
|
||||
### Your One Success:
|
||||
School "b" (tracking: `school_3387003d82`) got ID: `796GQ4VM8SWSQ`
|
||||
- This proves someone **completed the full onboarding**
|
||||
- That's why it worked for this one!
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist ✅
|
||||
|
||||
- [ ] Run `cleanup.php` to fix old data
|
||||
- [ ] Try new onboarding - **INCOMPLETE** (close browser early)
|
||||
- Should see: ⚠️ "Onboarding Incomplete" warning
|
||||
- Should NOT save merchant ID
|
||||
- [ ] Try new onboarding - **COMPLETE** (finish all steps)
|
||||
- Should see: ✅ "Onboarding completed"
|
||||
- Should save DIFFERENT merchant ID (not `NEG5HMUKCTQNL`)
|
||||
- [ ] Check `merchants.json` has correct IDs
|
||||
- [ ] Test creating an order with the new merchant
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference 📚
|
||||
|
||||
### Good Merchant ID:
|
||||
```json
|
||||
"merchant_id": "796GQ4VM8SWSQ" ✅ DIFFERENT from partner ID
|
||||
```
|
||||
|
||||
### Bad Merchant ID (Before Fix):
|
||||
```json
|
||||
"merchant_id": "NEG5HMUKCTQNL" ❌ This is YOUR partner ID!
|
||||
```
|
||||
|
||||
### Incomplete Status (After Fix):
|
||||
```json
|
||||
{
|
||||
"status": "incomplete_onboarding",
|
||||
"note": "Merchant did not complete PayPal onboarding"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Need Help? 🆘
|
||||
|
||||
1. **View diagnostics**: `http://localhost/payp/partner_diag.php`
|
||||
2. **Check debug logs**:
|
||||
- `data/return_debug.json`
|
||||
- `data/sync_debug.json`
|
||||
3. **Read full docs**: `README_ONBOARDING_FIX.md`
|
||||
|
||||
---
|
||||
|
||||
## Production Considerations ⚠️
|
||||
|
||||
For production (when you go live):
|
||||
|
||||
1. **User Education**: Tell merchants they MUST complete the full PayPal signup
|
||||
2. **Email Reminders**: Send follow-up if onboarding incomplete
|
||||
3. **Support Flow**: Have a process to help merchants who get stuck
|
||||
4. **Monitoring**: Watch for `incomplete_onboarding` status entries
|
||||
|
||||
---
|
||||
|
||||
**Bottom Line**: Your code is working! The issue was accepting the partner ID when merchants didn't finish signup. Now it's fixed and you'll see clear warnings when onboarding is incomplete. 🎉
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
$config = require __DIR__ . '/config.php';
|
||||
require __DIR__ . '/lib/PayPalClient.php';
|
||||
require __DIR__ . '/lib/Storage.php';
|
||||
|
||||
$orderId = $_GET['token'] ?? $_GET['orderId'] ?? null; // token param from PayPal return
|
||||
if (!$orderId) {
|
||||
http_response_code(400);
|
||||
echo 'Missing orderId/token';
|
||||
exit;
|
||||
}
|
||||
|
||||
$client = new PayPalClient($config);
|
||||
$storage = new Storage($config['storage_path']);
|
||||
|
||||
// Capture the order
|
||||
$resp = $client->api('POST', '/v2/checkout/orders/' . urlencode($orderId) . '/capture');
|
||||
|
||||
if ($resp['status'] < 200 || $resp['status'] >= 300) {
|
||||
http_response_code(500);
|
||||
echo 'Capture failed: ' . htmlspecialchars(json_encode($resp));
|
||||
exit;
|
||||
}
|
||||
|
||||
$capture = $resp['body'];
|
||||
|
||||
// Update local order store
|
||||
$orders = $storage->get('orders', []);
|
||||
if (isset($orders[$orderId])) {
|
||||
$orders[$orderId]['status'] = $capture['status'] ?? 'CAPTURED';
|
||||
$storage->set('orders', $orders);
|
||||
}
|
||||
|
||||
// Redirect back to home with a status indicator
|
||||
$baseUrl = rtrim($config['base_url'], '/');
|
||||
$status = isset($capture['status']) ? $capture['status'] : 'CAPTURED';
|
||||
header('Location: ' . $baseUrl . '/index.php?captured=1&order=' . urlencode($orderId) . '&status=' . urlencode($status));
|
||||
exit;
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
/**
|
||||
* Cleanup utility to remove incorrect merchant IDs (partner IDs saved as merchant IDs)
|
||||
* Run this once to clean up the bad data from before the fix
|
||||
*/
|
||||
|
||||
$config = require __DIR__ . '/config.php';
|
||||
require __DIR__ . '/lib/Storage.php';
|
||||
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
|
||||
$storage = new Storage($config['storage_path']);
|
||||
$partnerMerchantId = $config['partner_merchant_id'];
|
||||
|
||||
$cleaned = [
|
||||
'merchants' => 0,
|
||||
'pending' => 0,
|
||||
];
|
||||
|
||||
// Clean merchants.json
|
||||
$merchants = $storage->get('merchants', []);
|
||||
$originalMerchantsCount = count($merchants);
|
||||
foreach ($merchants as $school => $info) {
|
||||
if (isset($info['merchant_id']) && strcasecmp($info['merchant_id'], $partnerMerchantId) === 0) {
|
||||
unset($merchants[$school]);
|
||||
$cleaned['merchants']++;
|
||||
}
|
||||
}
|
||||
if ($cleaned['merchants'] > 0) {
|
||||
$storage->set('merchants', $merchants);
|
||||
}
|
||||
|
||||
// Clean pending_onboarding.json
|
||||
$pending = $storage->get('pending_onboarding', []);
|
||||
foreach ($pending as $trackId => &$entry) {
|
||||
if (!is_array($entry)) continue;
|
||||
|
||||
if (isset($entry['merchant_id']) && strcasecmp($entry['merchant_id'], $partnerMerchantId) === 0) {
|
||||
// Remove the incorrect merchant_id and mark as incomplete
|
||||
unset($entry['merchant_id']);
|
||||
$entry['status'] = 'incomplete_onboarding';
|
||||
$entry['note'] = 'Removed incorrect partner ID. Merchant needs to complete onboarding.';
|
||||
$entry['cleaned_at'] = time();
|
||||
$cleaned['pending']++;
|
||||
}
|
||||
}
|
||||
unset($entry);
|
||||
if ($cleaned['pending'] > 0) {
|
||||
$storage->set('pending_onboarding', $pending);
|
||||
}
|
||||
|
||||
?><!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Cleanup Utility - PayPal Integration</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
|
||||
.box { border: 1px solid #ddd; padding: 1.5rem; border-radius: .5rem; margin-bottom: 1rem; }
|
||||
.success { background: #d4edda; border-color: #c3e6cb; color: #155724; }
|
||||
.warning { background: #fff3cd; border-color: #ffeaa7; color: #856404; }
|
||||
.info { background: #d1ecf1; border-color: #bee5eb; color: #0c5460; }
|
||||
code { background: #f4f4f4; padding: .2rem .4rem; border-radius: .25rem; }
|
||||
a { color: #0070ba; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🧹 Data Cleanup Complete</h1>
|
||||
|
||||
<?php if ($cleaned['merchants'] > 0 || $cleaned['pending'] > 0): ?>
|
||||
<div class="box success">
|
||||
<h2>✅ Cleanup Results</h2>
|
||||
<p><strong><?= $cleaned['merchants'] + $cleaned['pending'] ?></strong> incorrect entries fixed:</p>
|
||||
<ul>
|
||||
<li><strong><?= $cleaned['merchants'] ?></strong> incorrect merchant entries removed from <code>merchants.json</code></li>
|
||||
<li><strong><?= $cleaned['pending'] ?></strong> pending entries corrected in <code>pending_onboarding.json</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="box info">
|
||||
<h2>ℹ️ No Cleanup Needed</h2>
|
||||
<p>No incorrect merchant IDs found. Your data is clean!</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="box warning">
|
||||
<h3>⚠️ About the Cleanup</h3>
|
||||
<p>This utility removed entries where your <strong>partner merchant ID</strong> (<code><?= htmlspecialchars($partnerMerchantId) ?></code>) was incorrectly saved as a merchant's ID.</p>
|
||||
<p>These merchants will need to:</p>
|
||||
<ol>
|
||||
<li>Click "Connect with PayPal" again</li>
|
||||
<li><strong>Complete the ENTIRE PayPal business account setup</strong></li>
|
||||
<li>Return to your application to get properly connected</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="box info">
|
||||
<h3>📊 Current Status</h3>
|
||||
<p><strong>Total connected merchants:</strong> <?= count($merchants) ?></p>
|
||||
<p><strong>Total pending entries:</strong> <?= count($pending) ?></p>
|
||||
|
||||
<?php if (count($merchants) > 0): ?>
|
||||
<h4>Connected Merchants:</h4>
|
||||
<ul>
|
||||
<?php foreach ($merchants as $school => $info): ?>
|
||||
<li><strong><?= htmlspecialchars($school) ?></strong> - <code><?= htmlspecialchars($info['merchant_id']) ?></code></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3>🔗 Next Steps</h3>
|
||||
<ul>
|
||||
<li><a href="index.php">← Back to Dashboard</a></li>
|
||||
<li><a href="partner_diag.php" target="_blank">View Partner Diagnostics</a></li>
|
||||
</ul>
|
||||
<p><small><strong>Note:</strong> You can safely run this cleanup script multiple times. It only removes entries that match your partner merchant ID.</small></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
// Basic configuration for PayPal integration.
|
||||
// Fill in your Sandbox (or Live) credentials from developer.paypal.com
|
||||
|
||||
return [
|
||||
// 'sandbox' or 'live'
|
||||
'env' => 'sandbox',
|
||||
|
||||
// PayPal REST App credentials for your Partner (platform) app
|
||||
'client_id' => 'AZ0RTbfvdUAECrswIOBVnSzLMBgvAzv8qI56YfXPGN46ukzx4qhmeflsOodvzNHE7wd5MRXupBxFmvr0',
|
||||
'client_secret' => 'EN-EaI-l4s9_CQOWzvLhVa-N1JHMUWjR22b3kfn3u8q5IHa3-hGRe6X8Sh_0LRc3jZyGTKvsV3QjeW5U',
|
||||
|
||||
// Your Partner's PayPal Merchant ID (aka Payer ID). Required to look up merchant info by tracking_id
|
||||
// Find this in Business Profile > Account IDs in your PayPal dashboard (sandbox or live as appropriate)
|
||||
'partner_merchant_id' => 'NEG5HMUKCTQNL',
|
||||
|
||||
// Your base URL as accessed by the browser. Adjust port if using a different Apache port.
|
||||
// For XAMPP default: http://localhost/payp
|
||||
'base_url' => 'http://localhost/payp',
|
||||
|
||||
// Storage path (must be writable by Apache/PHP)
|
||||
'storage_path' => __DIR__ . DIRECTORY_SEPARATOR . 'data',
|
||||
];
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
$config = require __DIR__ . '/config.php';
|
||||
require __DIR__ . '/lib/PayPalClient.php';
|
||||
require __DIR__ . '/lib/Storage.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo 'Method Not Allowed';
|
||||
exit;
|
||||
}
|
||||
|
||||
$school = trim($_POST['school'] ?? '');
|
||||
$amount = (string)($_POST['amount'] ?? '0');
|
||||
|
||||
if ($school === '' || !is_numeric($amount) || (float)$amount <= 0) {
|
||||
http_response_code(400);
|
||||
echo 'Invalid input';
|
||||
exit;
|
||||
}
|
||||
|
||||
$storage = new Storage($config['storage_path']);
|
||||
$merchants = $storage->get('merchants', []);
|
||||
$merchantId = $merchants[$school]['merchant_id'] ?? null;
|
||||
if (!$merchantId) {
|
||||
http_response_code(400);
|
||||
echo 'School not onboarded';
|
||||
exit;
|
||||
}
|
||||
|
||||
$client = new PayPalClient($config);
|
||||
|
||||
$returnUrl = rtrim($config['base_url'], '/') . '/capture_order.php';
|
||||
$cancelUrl = rtrim($config['base_url'], '/') . '/index.php';
|
||||
|
||||
// Orders v2 create, routing funds to payee.merchant_id
|
||||
$payload = [
|
||||
'intent' => 'CAPTURE',
|
||||
'purchase_units' => [[
|
||||
'amount' => [
|
||||
'currency_code' => 'USD',
|
||||
'value' => number_format((float)$amount, 2, '.', ''),
|
||||
],
|
||||
'payee' => [
|
||||
'merchant_id' => $merchantId,
|
||||
],
|
||||
'description' => 'School Lunch Payment for ' . $school,
|
||||
]],
|
||||
'application_context' => [
|
||||
'brand_name' => 'School Lunch Service',
|
||||
'landing_page' => 'LOGIN',
|
||||
'shipping_preference' => 'NO_SHIPPING',
|
||||
'user_action' => 'PAY_NOW',
|
||||
'return_url' => $returnUrl,
|
||||
'cancel_url' => $cancelUrl,
|
||||
],
|
||||
];
|
||||
|
||||
$resp = $client->api('POST', '/v2/checkout/orders', $payload);
|
||||
if ($resp['status'] < 200 || $resp['status'] >= 300) {
|
||||
http_response_code(500);
|
||||
echo 'Create order failed: ' . htmlspecialchars(json_encode($resp));
|
||||
exit;
|
||||
}
|
||||
|
||||
$order = $resp['body'];
|
||||
$approve = null;
|
||||
foreach (($order['links'] ?? []) as $lnk) {
|
||||
if (($lnk['rel'] ?? '') === 'approve') { $approve = $lnk['href']; break; }
|
||||
}
|
||||
|
||||
if (!$approve) {
|
||||
http_response_code(500);
|
||||
echo 'Missing approval link';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Save a minimal order record for demo
|
||||
$orders = $storage->get('orders', []);
|
||||
$orders[$order['id']] = [
|
||||
'school' => $school,
|
||||
'amount' => $payload['purchase_units'][0]['amount']['value'],
|
||||
'merchant_id' => $merchantId,
|
||||
'status' => 'CREATED',
|
||||
];
|
||||
$storage->set('orders', $orders);
|
||||
|
||||
header('Location: ' . $approve);
|
||||
exit;
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"b": {
|
||||
"merchant_id": "796GQ4VM8SWSQ",
|
||||
"tracking_id": "school_3387003d82",
|
||||
"connected_at": 1760885871
|
||||
},
|
||||
"lf": {
|
||||
"merchant_id": "7A68CU68VGK8J",
|
||||
"tracking_id": "school_b97918b706",
|
||||
"connected_at": 1760889241
|
||||
},
|
||||
"lyc\u00e9e": {
|
||||
"merchant_id": "7A68CU68VGK8J",
|
||||
"tracking_id": "school_44045be337",
|
||||
"connected_at": 1760889638
|
||||
},
|
||||
"testing": {
|
||||
"merchant_id": "7A68CU68VGK8J",
|
||||
"tracking_id": "school_08b8491e27",
|
||||
"connected_at": 1760895157
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"1FC77406FJ583553M": {
|
||||
"school": "lf",
|
||||
"amount": "5.00",
|
||||
"merchant_id": "7A68CU68VGK8J",
|
||||
"status": "CREATED"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"school_a0e12d4318": {
|
||||
"school": "aa",
|
||||
"created_at": 1760886320,
|
||||
"partner_referral_id": "ODRiNWQ0NDEtODE0NC00ZTE1LWIzOTEtNTU1MTg3Njk3MWQ2N1FUWkFmRENxc3lJQjNFUjloZEx6VHRjVGNMQkJpN2ZoVXVLNTFXc1p1RT12Mg==",
|
||||
"referral_token": "ODRiNWQ0NDEtODE0NC00ZTE1LWIzOTEtNTU1MTg3Njk3MWQ2N1FUWkFmRENxc3lJQjNFUjloZEx6VHRjVGNMQkJpN2ZoVXVLNTFXc1p1RT12Mg==",
|
||||
"status": "incomplete_onboarding",
|
||||
"last_sync_at": 1760895145,
|
||||
"note": "Merchant has not completed PayPal onboarding. Only partner ID is available."
|
||||
},
|
||||
"school_b97918b706": {
|
||||
"school": "lf",
|
||||
"created_at": 1760889168,
|
||||
"partner_referral_id": "M2QzOTkwZmEtYmFhNS00NTc2LTk0OTMtYzlkZDExNTJmMDI5K1F1bEVCalQ2cG5xRTJCTVQ5RERPWnZvLzZQelVndGdDQkMvbytHMENMND12Mg==",
|
||||
"referral_token": "M2QzOTkwZmEtYmFhNS00NTc2LTk0OTMtYzlkZDExNTJmMDI5K1F1bEVCalQ2cG5xRTJCTVQ5RERPWnZvLzZQelVndGdDQkMvbytHMENMND12Mg==",
|
||||
"merchant_id": "7A68CU68VGK8J",
|
||||
"status": "connected",
|
||||
"connected_at": 1760889241
|
||||
},
|
||||
"school_6d371a110a": {
|
||||
"school": "jjj",
|
||||
"created_at": 1760889277,
|
||||
"partner_referral_id": "NTYzYzEwZjAtMGM0NS00ZjU2LTg5ZGUtMTNiYWVlYWMxMjdmYmcyZ2M4dDBrRWJwYzJDcUdYQ3Y0SHFUU0NjUjZoa0dIdGo4N25EUnZDbz12Mg==",
|
||||
"referral_token": "NTYzYzEwZjAtMGM0NS00ZjU2LTg5ZGUtMTNiYWVlYWMxMjdmYmcyZ2M4dDBrRWJwYzJDcUdYQ3Y0SHFUU0NjUjZoa0dIdGo4N25EUnZDbz12Mg==",
|
||||
"status": "incomplete_onboarding",
|
||||
"last_sync_at": 1760895149,
|
||||
"note": "Merchant has not completed PayPal onboarding. Only partner ID is available."
|
||||
},
|
||||
"school_c7bc4cfab7": {
|
||||
"school": "lyc\u00e9e france",
|
||||
"created_at": 1760889357,
|
||||
"partner_referral_id": "ODc5MTljZDUtY2Y4My00NGZjLWJjMDItM2EzZWIyZjFlOTkxdDcwTkRIY0E5UEgrR3ZHZ24wK1VCVjYvR3l6S2FHNmNPSDlTMUZOSWdwST12Mg==",
|
||||
"referral_token": "ODc5MTljZDUtY2Y4My00NGZjLWJjMDItM2EzZWIyZjFlOTkxdDcwTkRIY0E5UEgrR3ZHZ24wK1VCVjYvR3l6S2FHNmNPSDlTMUZOSWdwST12Mg==",
|
||||
"status": "incomplete_onboarding",
|
||||
"last_sync_at": 1760895155,
|
||||
"note": "Merchant has not completed PayPal onboarding. Only partner ID is available."
|
||||
},
|
||||
"school_44045be337": {
|
||||
"school": "lyc\u00e9e",
|
||||
"created_at": 1760889386,
|
||||
"partner_referral_id": "ODZlZTU3ODctZWMwMy00MmY4LWIyYWItZDhiYjk3OTA0ZWMwZ21zRjZtdjVRb0JOR2g3L2JiY29MNnluNFRpL3BZcm1lTENHRDF3TTZnMD12Mg==",
|
||||
"referral_token": "ODZlZTU3ODctZWMwMy00MmY4LWIyYWItZDhiYjk3OTA0ZWMwZ21zRjZtdjVRb0JOR2g3L2JiY29MNnluNFRpL3BZcm1lTENHRDF3TTZnMD12Mg==",
|
||||
"merchant_id": "7A68CU68VGK8J",
|
||||
"status": "connected",
|
||||
"connected_at": 1760889638
|
||||
},
|
||||
"school_08b8491e27": {
|
||||
"school": "testing",
|
||||
"created_at": 1760895090,
|
||||
"partner_referral_id": "MzIwZjhlZGYtMWEyZS00NjQzLTgzNDgtYmU2OTE4OTcxZGJjSE9IWlNlbjhoVVduc0tqUk53bTR0and5VjN6K2xUREZpekN3MUY0ckRGND12Mg==",
|
||||
"referral_token": "MzIwZjhlZGYtMWEyZS00NjQzLTgzNDgtYmU2OTE4OTcxZGJjSE9IWlNlbjhoVVduc0tqUk53bTR0and5VjN6K2xUREZpekN3MUY0ckRGND12Mg==",
|
||||
"merchant_id": "7A68CU68VGK8J",
|
||||
"status": "connected",
|
||||
"connected_at": 1760895157
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
[
|
||||
{
|
||||
"ts": 1760741267,
|
||||
"school": "aq",
|
||||
"tracking_id": "school_84f87ff983",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/MWMzYTk3OTItOWIwOC00YzQyLTlkZWMtYmE1YTAxMGI0ZTU1OVB4aFdySHdjVGRJQkJqbUNtZ3VLV2ZpN3lMYUpRREdBVzlyZG5rVndyST12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=MWMzYTk3OTItOWIwOC00YzQyLTlkZWMtYmE1YTAxMGI0ZTU1OVB4aFdySHdjVGRJQkJqbUNtZ3VLV2ZpN3lMYUpRREdBVzlyZG5rVndyST12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760743658,
|
||||
"school": "g",
|
||||
"tracking_id": "school_1d70dd81c8",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/ODQxM2MzYjItMDdhYi00ODY0LWIyMDYtYjRjMzIwNjBlZWFjWkRYTUZzWWdZcUdyYkZTMEhmSU1Pa21MMUZ3RUF6YmszZjNJMGJxTlZqWT12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=ODQxM2MzYjItMDdhYi00ODY0LWIyMDYtYjRjMzIwNjBlZWFjWkRYTUZzWWdZcUdyYkZTMEhmSU1Pa21MMUZ3RUF6YmszZjNJMGJxTlZqWT12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760744309,
|
||||
"school": "v",
|
||||
"tracking_id": "school_151a17f8e4",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/YmFkYjNhNDItNmUxYS00MmJkLWExMjktZThmNGZkNzJiNjY0VFk3cURnd2RkMmRxK29EaStpeFBxZG1sZXBWOS9xaVFZdnFoQnhMK0RSTT12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=YmFkYjNhNDItNmUxYS00MmJkLWExMjktZThmNGZkNzJiNjY0VFk3cURnd2RkMmRxK29EaStpeFBxZG1sZXBWOS9xaVFZdnFoQnhMK0RSTT12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760794408,
|
||||
"school": "ggggggggg",
|
||||
"tracking_id": "school_c6bf6ca93c",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/ZmQzZDk5ZDYtYTY1MC00ZjQyLWJhZmItOGYzOGM2NzM1ODkwSnFmUDBuOGsrd3Q1cVlENTR4Nmp6TWl1aWM2QXIrL0xFaDBheGpiY0JFZz12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=ZmQzZDk5ZDYtYTY1MC00ZjQyLWJhZmItOGYzOGM2NzM1ODkwSnFmUDBuOGsrd3Q1cVlENTR4Nmp6TWl1aWM2QXIrL0xFaDBheGpiY0JFZz12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760795720,
|
||||
"school": "s",
|
||||
"tracking_id": "school_dfd4578e02",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/YjJjNzQ0ZGEtMTg3Zi00NTBiLTkyYmUtNGM0YmE0MTlhNDE4K0hRQlllMTBTY2RRaWtjNGUrZllJOEp4Q0dpRXRoa3VoeEhlZVl5OFlZYz12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=YjJjNzQ0ZGEtMTg3Zi00NTBiLTkyYmUtNGM0YmE0MTlhNDE4K0hRQlllMTBTY2RRaWtjNGUrZllJOEp4Q0dpRXRoa3VoeEhlZVl5OFlZYz12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760796067,
|
||||
"school": "m",
|
||||
"tracking_id": "school_c626754298",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/MjkzOTU5ZTgtMjc5Yy00YzA4LThlMDktOTZlNjZkOTA5Mjk3M3hNRFpORHFtRVNEWVJJRDBDTmpETC84dm9VcGUzRGpSWEJWSDlHV3hZUT12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=MjkzOTU5ZTgtMjc5Yy00YzA4LThlMDktOTZlNjZkOTA5Mjk3M3hNRFpORHFtRVNEWVJJRDBDTmpETC84dm9VcGUzRGpSWEJWSDlHV3hZUT12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760796068,
|
||||
"school": "m",
|
||||
"tracking_id": "school_c626754298",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/OGI3MjZkZGMtNDJhNS00OTZjLThhYjItNGFiM2ZmMDA0NjcwdVBGTVF0UXkzeUtyRFA4dFI0Mzk5Z3NRaGpRN3ZScDkzZ1Q4OHgvY2VCST12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=OGI3MjZkZGMtNDJhNS00OTZjLThhYjItNGFiM2ZmMDA0NjcwdVBGTVF0UXkzeUtyRFA4dFI0Mzk5Z3NRaGpRN3ZScDkzZ1Q4OHgvY2VCST12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760796822,
|
||||
"school": "ml",
|
||||
"tracking_id": "school_c6fbbf852c",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/MTU4MzNhNjItNTkxMC00YTdlLWJjMjgtMWM4OTA5Zjg2MzgzYmE3elZRalRYNVpVYzFJWTBxdG5RbGF6UXNQclNUWFkzL2p4K3ZrVFEvND12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=MTU4MzNhNjItNTkxMC00YTdlLWJjMjgtMWM4OTA5Zjg2MzgzYmE3elZRalRYNVpVYzFJWTBxdG5RbGF6UXNQclNUWFkzL2p4K3ZrVFEvND12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760797314,
|
||||
"school": "b",
|
||||
"tracking_id": "school_3387003d82",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/MTQ2NjlmZTYtNjBlNy00YmU1LTg5MzQtMjRjNzU3NDAwMDgzN1dkZkl1NFM2bkZ4SmwrMGdUbWF1TUpYc2tjeHZ3Q241QmNCYkNQVktVTT12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=MTQ2NjlmZTYtNjBlNy00YmU1LTg5MzQtMjRjNzU3NDAwMDgzN1dkZkl1NFM2bkZ4SmwrMGdUbWF1TUpYc2tjeHZ3Q241QmNCYkNQVktVTT12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760884455,
|
||||
"school": "pol",
|
||||
"tracking_id": "school_a5a7e52f14",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/NjU5NGZlYTgtNzE5ZC00OGYyLWFmOGYtYTA2ZjU3M2VmMTA3ZW5JTkNwdVp6NXZQbW1xWC9Mak5CcFh2dG9TdGVqdHdRSWU0R3BtNUtYOD12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=NjU5NGZlYTgtNzE5ZC00OGYyLWFmOGYtYTA2ZjU3M2VmMTA3ZW5JTkNwdVp6NXZQbW1xWC9Mak5CcFh2dG9TdGVqdHdRSWU0R3BtNUtYOD12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760884706,
|
||||
"school": "m",
|
||||
"tracking_id": "school_ab32258ab0",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/YzU2NGVlMWEtYTIzYy00ZmRlLWE3ZmYtYjA2YjQ3NmE4Mzk3bVcyTFR4T2UrMHJvMkU3WmZOUklvUjVJSTZzU2JQMkF4VmVJT2FmV0JHND12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=YzU2NGVlMWEtYTIzYy00ZmRlLWE3ZmYtYjA2YjQ3NmE4Mzk3bVcyTFR4T2UrMHJvMkU3WmZOUklvUjVJSTZzU2JQMkF4VmVJT2FmV0JHND12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760885845,
|
||||
"school": "sss",
|
||||
"tracking_id": "school_486d3e9b54",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/YjU1MWExZmMtMjM2MS00NWU2LWE4ZDMtNTQ4NzA4NzNhOWVlOVpWc29ValdqT240d3kwcXpaQmFJRWpZcGs5UjJzcmhiOU9MbythdkFSVT12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=YjU1MWExZmMtMjM2MS00NWU2LWE4ZDMtNTQ4NzA4NzNhOWVlOVpWc29ValdqT240d3kwcXpaQmFJRWpZcGs5UjJzcmhiOU9MbythdkFSVT12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760885872,
|
||||
"school": "sssssss",
|
||||
"tracking_id": "school_26fa793cb8",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/MjNmNGY2ZjUtODE5My00ODA4LWFiOGMtMTA1YzcxNzk2MmJjeEJnQ3EwejFjd2doa3BYQkVDSEx1dmV4a1FlalNjV1ZLYW9IZHRScktKcz12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=MjNmNGY2ZjUtODE5My00ODA4LWFiOGMtMTA1YzcxNzk2MmJjeEJnQ3EwejFjd2doa3BYQkVDSEx1dmV4a1FlalNjV1ZLYW9IZHRScktKcz12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760885933,
|
||||
"school": "mmmmmmmmmmmmmmmmmmmmmmm",
|
||||
"tracking_id": "school_a963bb26d0",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/YmEwZGI3ZjgtYjFiNi00MjU1LTg3Y2UtYTBkYjY0ODE2YmFhZHh3UHpKVDg3clJ1cDA1eXNwcWJpSFFpZkFtQjRNMXpQRHJHMjRjSllYWT12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=YmEwZGI3ZjgtYjFiNi00MjU1LTg3Y2UtYTBkYjY0ODE2YmFhZHh3UHpKVDg3clJ1cDA1eXNwcWJpSFFpZkFtQjRNMXpQRHJHMjRjSllYWT12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760886144,
|
||||
"school": "new",
|
||||
"tracking_id": "school_5d71344bab",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/YjA1YzI4NDYtNjUyMS00NDI4LWIxZTYtODFjNzY1MzFlY2FjdWZQcHVJdng3UUxBUmQ4YWlBSEUzejhFZEZjQXdTUmVqaGhLNGFCOVJxYz12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=YjA1YzI4NDYtNjUyMS00NDI4LWIxZTYtODFjNzY1MzFlY2FjdWZQcHVJdng3UUxBUmQ4YWlBSEUzejhFZEZjQXdTUmVqaGhLNGFCOVJxYz12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760886320,
|
||||
"school": "aa",
|
||||
"tracking_id": "school_a0e12d4318",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/ODRiNWQ0NDEtODE0NC00ZTE1LWIzOTEtNTU1MTg3Njk3MWQ2N1FUWkFmRENxc3lJQjNFUjloZEx6VHRjVGNMQkJpN2ZoVXVLNTFXc1p1RT12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=ODRiNWQ0NDEtODE0NC00ZTE1LWIzOTEtNTU1MTg3Njk3MWQ2N1FUWkFmRENxc3lJQjNFUjloZEx6VHRjVGNMQkJpN2ZoVXVLNTFXc1p1RT12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760889168,
|
||||
"school": "lf",
|
||||
"tracking_id": "school_b97918b706",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/M2QzOTkwZmEtYmFhNS00NTc2LTk0OTMtYzlkZDExNTJmMDI5K1F1bEVCalQ2cG5xRTJCTVQ5RERPWnZvLzZQelVndGdDQkMvbytHMENMND12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=M2QzOTkwZmEtYmFhNS00NTc2LTk0OTMtYzlkZDExNTJmMDI5K1F1bEVCalQ2cG5xRTJCTVQ5RERPWnZvLzZQelVndGdDQkMvbytHMENMND12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760889277,
|
||||
"school": "jjj",
|
||||
"tracking_id": "school_6d371a110a",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/NTYzYzEwZjAtMGM0NS00ZjU2LTg5ZGUtMTNiYWVlYWMxMjdmYmcyZ2M4dDBrRWJwYzJDcUdYQ3Y0SHFUU0NjUjZoa0dIdGo4N25EUnZDbz12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=NTYzYzEwZjAtMGM0NS00ZjU2LTg5ZGUtMTNiYWVlYWMxMjdmYmcyZ2M4dDBrRWJwYzJDcUdYQ3Y0SHFUU0NjUjZoa0dIdGo4N25EUnZDbz12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760889357,
|
||||
"school": "lyc\u00e9e france",
|
||||
"tracking_id": "school_c7bc4cfab7",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/ODc5MTljZDUtY2Y4My00NGZjLWJjMDItM2EzZWIyZjFlOTkxdDcwTkRIY0E5UEgrR3ZHZ24wK1VCVjYvR3l6S2FHNmNPSDlTMUZOSWdwST12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=ODc5MTljZDUtY2Y4My00NGZjLWJjMDItM2EzZWIyZjFlOTkxdDcwTkRIY0E5UEgrR3ZHZ24wK1VCVjYvR3l6S2FHNmNPSDlTMUZOSWdwST12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760889386,
|
||||
"school": "lyc\u00e9e",
|
||||
"tracking_id": "school_44045be337",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/ODZlZTU3ODctZWMwMy00MmY4LWIyYWItZDhiYjk3OTA0ZWMwZ21zRjZtdjVRb0JOR2g3L2JiY29MNnluNFRpL3BZcm1lTENHRDF3TTZnMD12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=ODZlZTU3ODctZWMwMy00MmY4LWIyYWItZDhiYjk3OTA0ZWMwZ21zRjZtdjVRb0JOR2g3L2JiY29MNnluNFRpL3BZcm1lTENHRDF3TTZnMD12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": 1760895090,
|
||||
"school": "testing",
|
||||
"tracking_id": "school_08b8491e27",
|
||||
"status": 201,
|
||||
"body": {
|
||||
"links": [
|
||||
{
|
||||
"href": "https:\/\/api.sandbox.paypal.com\/v2\/customer\/partner-referrals\/MzIwZjhlZGYtMWEyZS00NjQzLTgzNDgtYmU2OTE4OTcxZGJjSE9IWlNlbjhoVVduc0tqUk53bTR0and5VjN6K2xUREZpekN3MUY0ckRGND12Mg==",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"description": "Read Referral Data shared by the Caller."
|
||||
},
|
||||
{
|
||||
"href": "https:\/\/www.sandbox.paypal.com\/bizsignup\/partner\/entry?referralToken=MzIwZjhlZGYtMWEyZS00NjQzLTgzNDgtYmU2OTE4OTcxZGJjSE9IWlNlbjhoVVduc0tqUk53bTR0and5VjN6K2xUREZpekN3MUY0ckRGND12Mg==",
|
||||
"rel": "action_url",
|
||||
"method": "GET",
|
||||
"description": "Target WEB REDIRECT URL for the next action. Customer should be redirected to this URL in the browser."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
[
|
||||
{
|
||||
"ts": 1760884801,
|
||||
"query": {
|
||||
"school": "pol",
|
||||
"track": "school_a5a7e52f14"
|
||||
},
|
||||
"last_status": 200,
|
||||
"last_body": {
|
||||
"merchant_id": "NEG5HMUKCTQNL",
|
||||
"tracking_id": "school_a5a7e52f14",
|
||||
"links": [
|
||||
{
|
||||
"href": "\/v1\/customer\/partners\/NEG5HMUKCTQNL\/merchant-integrations\/NEG5HMUKCTQNL",
|
||||
"rel": "get",
|
||||
"method": "GET"
|
||||
}
|
||||
]
|
||||
},
|
||||
"track": "school_a5a7e52f14",
|
||||
"school": "pol"
|
||||
},
|
||||
{
|
||||
"ts": 1760884847,
|
||||
"query": {
|
||||
"school": "m",
|
||||
"track": "school_ab32258ab0"
|
||||
},
|
||||
"last_status": 200,
|
||||
"last_body": {
|
||||
"merchant_id": "NEG5HMUKCTQNL",
|
||||
"tracking_id": "school_ab32258ab0",
|
||||
"links": [
|
||||
{
|
||||
"href": "\/v1\/customer\/partners\/NEG5HMUKCTQNL\/merchant-integrations\/NEG5HMUKCTQNL",
|
||||
"rel": "get",
|
||||
"method": "GET"
|
||||
}
|
||||
]
|
||||
},
|
||||
"track": "school_ab32258ab0",
|
||||
"school": "m"
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large.
Load diff
+182
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
$config = require __DIR__ . '/config.php';
|
||||
require __DIR__ . '/lib/Storage.php';
|
||||
|
||||
$storage = new Storage($config['storage_path']);
|
||||
$merchants = $storage->get('merchants', []);
|
||||
|
||||
// Backfill: if pending_onboarding has connected entries with merchant_id, ensure they exist in merchants.json
|
||||
$pending = $storage->get('pending_onboarding', []);
|
||||
$changed = false;
|
||||
foreach ($pending as $trackId => $entry) {
|
||||
if (!is_array($entry)) continue;
|
||||
$schoolName = $entry['school'] ?? null;
|
||||
$mid = $entry['merchant_id'] ?? null;
|
||||
$status = $entry['status'] ?? null;
|
||||
if ($schoolName && $mid && strtolower((string)$status) === 'connected') {
|
||||
if (!isset($merchants[$schoolName]) || empty($merchants[$schoolName]['merchant_id'])) {
|
||||
$merchants[$schoolName] = [
|
||||
'merchant_id' => $mid,
|
||||
'tracking_id' => $trackId,
|
||||
'connected_at' => $entry['connected_at'] ?? time(),
|
||||
];
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($changed) {
|
||||
$storage->set('merchants', $merchants);
|
||||
}
|
||||
|
||||
?><!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>School Lunch PayPal Demo</title>
|
||||
<style>
|
||||
body{font-family:system-ui,Segoe UI,Arial,sans-serif; margin:2rem;}
|
||||
.box{border:1px solid #ddd; padding:1rem; border-radius:.5rem; margin-bottom:1rem;}
|
||||
label{display:block; margin:.5rem 0 .25rem;}
|
||||
input,select,button{padding:.5rem; font-size:1rem;}
|
||||
table{border-collapse:collapse; width:100%;}
|
||||
th,td{border:1px solid #eee; padding:.5rem;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>School Lunch Payments (Demo)</h1>
|
||||
|
||||
<div class="box">
|
||||
<h2>Connect a School's PayPal (Merchant Onboarding)</h2>
|
||||
<form method="post" action="/payp/onboard.php">
|
||||
<label>School Name</label>
|
||||
<input name="school_name" placeholder="e.g., Green Valley Elementary" required />
|
||||
<button type="submit">Connect with PayPal</button>
|
||||
|
||||
<div class="box">
|
||||
<h2>Pending Onboarding</h2>
|
||||
<?php
|
||||
// Show entries that do not yet have a merchant_id saved
|
||||
$pendingList = [];
|
||||
$incompleteList = [];
|
||||
foreach ($pending as $trackId => $entry) {
|
||||
if (!is_array($entry)) continue;
|
||||
$status = $entry['status'] ?? null;
|
||||
if ($status === 'incomplete_onboarding') {
|
||||
$incompleteList[$trackId] = $entry;
|
||||
} elseif (empty($entry['merchant_id'])) {
|
||||
$pendingList[$trackId] = $entry;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<?php if ($incompleteList): ?>
|
||||
<div style="background:#fff3cd; padding:1rem; margin-bottom:1rem; border-radius:.5rem;">
|
||||
<h3 style="margin-top:0; color:#856404;">⚠️ Incomplete Onboarding</h3>
|
||||
<p style="color:#856404;">The following merchants started but did NOT complete PayPal onboarding. They need to complete the signup process at PayPal.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>School</th>
|
||||
<th>Status</th>
|
||||
<th>Note</th>
|
||||
<th>Last Update</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($incompleteList as $trackId => $entry): ?>
|
||||
<tr style="background:#fff;">
|
||||
<td><?= htmlspecialchars($entry['school'] ?? '') ?></td>
|
||||
<td><span style="color:#856404;">❌ Incomplete</span></td>
|
||||
<td><small><?= htmlspecialchars($entry['note'] ?? 'Onboarding not completed') ?></small></td>
|
||||
<td><?= isset($entry['last_return_at']) ? date('Y-m-d H:i:s', (int)$entry['last_return_at']) : (isset($entry['created_at']) ? date('Y-m-d H:i:s', (int)$entry['created_at']) : '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$pendingList): ?>
|
||||
<p><?= $incompleteList ? 'No other pending onboardings.' : 'No pending onboardings.' ?></p>
|
||||
<?php else: ?>
|
||||
<p>Sometimes PayPal takes a short time to propagate onboarding. Click Sync to resolve merchant IDs.</p>
|
||||
<p>
|
||||
<a href="/payp/sync_merchants.php" style="padding:.5rem 1rem; background:#0070ba; color:white; text-decoration:none; border-radius:.25rem;">🔄 Sync Now</a>
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>School</th>
|
||||
<th>Tracking ID</th>
|
||||
<th>Partner Referral</th>
|
||||
<th>Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($pendingList as $trackId => $entry): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($entry['school'] ?? '') ?></td>
|
||||
<td><code><?= htmlspecialchars($trackId) ?></code></td>
|
||||
<td><code><?= htmlspecialchars($entry['partner_referral_id'] ?? ($entry['referral_token'] ?? '')) ?></code></td>
|
||||
<td><?= isset($entry['created_at']) ? date('Y-m-d H:i:s', (int)$entry['created_at']) : '' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
<p>This uses PayPal Partner Referrals to onboard merchants and store their merchantId for routing payments.</p>
|
||||
<p><small>After connecting, you'll be redirected back here.</small></p>
|
||||
<p><a href="https://developer.paypal.com/docs/platforms/partner-referrals/" target="_blank">Docs</a></p>
|
||||
<?php if (isset($_GET['sync'])): ?>
|
||||
<p style="color:green">Sync completed. Updated <?= (int)$_GET['sync']; ?> record(s).</p>
|
||||
<?php endif; ?>
|
||||
<p><a href="/payp/partner_diag.php" target="_blank">Partner diagnostics</a></p>
|
||||
<?php if (isset($_GET['onboard']) && $_GET['onboard']==='ok'): ?>
|
||||
<p style="color:green;">✅ Onboarding completed and merchant saved.</p>
|
||||
<?php endif; ?>
|
||||
<?php if (isset($_GET['onboard']) && $_GET['onboard']==='incomplete'): ?>
|
||||
<p style="color:#856404; background:#fff3cd; padding:1rem; border-radius:.5rem;">
|
||||
⚠️ <strong>Onboarding Incomplete:</strong> The merchant started the PayPal signup process but did not complete it.
|
||||
Please finish the entire PayPal business account setup to get your merchant ID. You can try connecting again.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php if (isset($_GET['onboard']) && $_GET['onboard']==='pending'): ?>
|
||||
<p style="color:#0070ba; background:#e7f3ff; padding:1rem; border-radius:.5rem;">
|
||||
ℹ️ <strong>Onboarding Pending:</strong> Your merchant ID is not available yet. This might take a few moments after completing PayPal signup.
|
||||
Try clicking "Sync Now" in a minute or refresh this page.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php if (isset($_GET['captured']) && $_GET['captured'] === '1'): ?>
|
||||
<p style="color:green">Payment captured successfully. Order <?= htmlspecialchars($_GET['order'] ?? '') ?> (status: <?= htmlspecialchars($_GET['status'] ?? 'CAPTURED') ?>)</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2>Connected Schools</h2>
|
||||
<?php if (!$merchants): ?>
|
||||
<p>No schools connected yet.</p>
|
||||
<?php else: ?>
|
||||
<table>
|
||||
<thead><tr><th>School</th><th>Merchant ID</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($merchants as $school => $info): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($school) ?></td>
|
||||
<td><code><?= htmlspecialchars($info['merchant_id'] ?? 'n/a') ?></code></td>
|
||||
<td>
|
||||
<form method="post" action="/payp/create_order.php" style="display:inline">
|
||||
<input type="hidden" name="school" value="<?= htmlspecialchars($school) ?>" />
|
||||
<input type="number" step="0.01" name="amount" value="5.00" style="width:7rem" />
|
||||
<button type="submit">Create Order</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
class PayPalClient
|
||||
{
|
||||
private string $clientId;
|
||||
private string $clientSecret;
|
||||
private string $env; // sandbox|live
|
||||
private string $baseApi;
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
$this->clientId = $config['client_id'] ?? '';
|
||||
$this->clientSecret = $config['client_secret'] ?? '';
|
||||
$this->env = $config['env'] ?? 'sandbox';
|
||||
$this->baseApi = $this->env === 'live'
|
||||
? 'https://api-m.paypal.com'
|
||||
: 'https://api-m.sandbox.paypal.com';
|
||||
|
||||
if (!$this->clientId || !$this->clientSecret) {
|
||||
throw new RuntimeException('Missing PayPal client_id/client_secret in config.php');
|
||||
}
|
||||
}
|
||||
|
||||
public function getAccessToken(): string
|
||||
{
|
||||
$url = $this->baseApi . '/v1/oauth2/token';
|
||||
$headers = [
|
||||
'Accept: application/json',
|
||||
'Accept-Language: en_US',
|
||||
];
|
||||
$body = 'grant_type=client_credentials';
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $body,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_USERPWD => $this->clientId . ':' . $this->clientSecret,
|
||||
]);
|
||||
$resp = curl_exec($ch);
|
||||
if ($resp === false) {
|
||||
throw new RuntimeException('OAuth token request failed: ' . curl_error($ch));
|
||||
}
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
$data = json_decode($resp, true);
|
||||
if ($code < 200 || $code >= 300 || !isset($data['access_token'])) {
|
||||
throw new RuntimeException('OAuth token response error (' . $code . '): ' . $resp);
|
||||
}
|
||||
return $data['access_token'];
|
||||
}
|
||||
|
||||
public function api(string $method, string $path, array $payload = null, array $headers = []): array
|
||||
{
|
||||
$url = $this->baseApi . $path;
|
||||
$accessToken = $this->getAccessToken();
|
||||
$defaultHeaders = [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $accessToken,
|
||||
];
|
||||
$allHeaders = array_merge($defaultHeaders, $headers);
|
||||
|
||||
$ch = curl_init($url);
|
||||
$opts = [
|
||||
CURLOPT_HTTPHEADER => $allHeaders,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CUSTOMREQUEST => strtoupper($method),
|
||||
];
|
||||
if ($payload !== null) {
|
||||
$opts[CURLOPT_POSTFIELDS] = json_encode($payload);
|
||||
}
|
||||
curl_setopt_array($ch, $opts);
|
||||
$resp = curl_exec($ch);
|
||||
if ($resp === false) {
|
||||
throw new RuntimeException('API request failed: ' . curl_error($ch));
|
||||
}
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
$data = json_decode($resp, true);
|
||||
if ($data === null && $resp !== '' && $resp !== 'null') {
|
||||
// not JSON but body present
|
||||
$data = ['raw' => $resp];
|
||||
}
|
||||
return ['status' => $code, 'body' => $data];
|
||||
}
|
||||
|
||||
public function getBaseApi(): string
|
||||
{
|
||||
return $this->baseApi;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
class Storage
|
||||
{
|
||||
private string $dir;
|
||||
|
||||
public function __construct(string $dir)
|
||||
{
|
||||
$this->dir = rtrim($dir, DIRECTORY_SEPARATOR);
|
||||
if (!is_dir($this->dir)) {
|
||||
mkdir($this->dir, 0777, true);
|
||||
}
|
||||
if (!is_writable($this->dir)) {
|
||||
throw new RuntimeException('Storage path not writable: ' . $this->dir);
|
||||
}
|
||||
}
|
||||
|
||||
private function path(string $name): string
|
||||
{
|
||||
return $this->dir . DIRECTORY_SEPARATOR . $name . '.json';
|
||||
}
|
||||
|
||||
public function get(string $name, $default = [])
|
||||
{
|
||||
$p = $this->path($name);
|
||||
if (!file_exists($p)) return $default;
|
||||
$data = json_decode((string)file_get_contents($p), true);
|
||||
return $data ?? $default;
|
||||
}
|
||||
|
||||
public function set(string $name, $value): void
|
||||
{
|
||||
$p = $this->path($name);
|
||||
file_put_contents($p, json_encode($value, JSON_PRETTY_PRINT));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
$config = require __DIR__ . '/config.php';
|
||||
require __DIR__ . '/lib/PayPalClient.php';
|
||||
require __DIR__ . '/lib/Storage.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo 'Method Not Allowed';
|
||||
exit;
|
||||
}
|
||||
|
||||
$schoolName = trim($_POST['school_name'] ?? '');
|
||||
if ($schoolName === '') {
|
||||
http_response_code(400);
|
||||
echo 'Missing school_name';
|
||||
exit;
|
||||
}
|
||||
|
||||
$client = new PayPalClient($config);
|
||||
$storage = new Storage($config['storage_path']);
|
||||
|
||||
$trackingId = 'school_' . substr(md5($schoolName . '|' . time()), 0, 10);
|
||||
|
||||
// IMPORTANT: Change return_url to be more explicit
|
||||
$returnUrl = rtrim($config['base_url'], '/') . '/return.php?school=' . urlencode($schoolName) . '&track=' . urlencode($trackingId);
|
||||
$cancelUrl = rtrim($config['base_url'], '/') . '/index.php';
|
||||
|
||||
$payload = [
|
||||
'tracking_id' => $trackingId,
|
||||
'operations' => [[
|
||||
'operation' => 'API_INTEGRATION',
|
||||
'api_integration_preference' => [
|
||||
'rest_api_integration' => [
|
||||
'integration_method' => 'PAYPAL',
|
||||
'integration_type' => 'THIRD_PARTY',
|
||||
'third_party_details' => [
|
||||
'features' => [
|
||||
'PAYMENT', 'REFUND'
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]],
|
||||
'products' => ['PPCP'],
|
||||
'legal_consents' => [[
|
||||
'type' => 'SHARE_DATA_CONSENT',
|
||||
'granted' => true
|
||||
]],
|
||||
'partner_config_override' => [
|
||||
'return_url' => $returnUrl,
|
||||
'return_url_description' => 'Return to School Lunch App',
|
||||
// Add this to ensure PayPal shows the return button
|
||||
'show_add_credit_card' => false,
|
||||
],
|
||||
'requested_capabilities' => [[
|
||||
'capability' => 'PAYMENTS_RECEIVABLE'
|
||||
]]
|
||||
];
|
||||
|
||||
// ...existing code...
|
||||
$resp = $client->api('POST', '/v2/customer/partner-referrals', $payload);
|
||||
if ($resp['status'] < 200 || $resp['status'] >= 300) {
|
||||
http_response_code(500);
|
||||
echo 'Partner referral failed: ' . htmlspecialchars(json_encode($resp));
|
||||
exit;
|
||||
}
|
||||
|
||||
$body = $resp['body'];
|
||||
$partnerReferralId = $body['partner_referral_id'] ?? ($body['referral_id'] ?? null);
|
||||
$referralToken = null;
|
||||
$links = $body['links'] ?? [];
|
||||
$href = null;
|
||||
|
||||
$debugReferral = $storage->get('referral_debug', []);
|
||||
$debugReferral[] = [
|
||||
'ts' => time(),
|
||||
'school' => $schoolName,
|
||||
'tracking_id' => $trackingId,
|
||||
'status' => $resp['status'],
|
||||
'body' => $body,
|
||||
];
|
||||
$storage->set('referral_debug', $debugReferral);
|
||||
|
||||
foreach ($links as $lnk) {
|
||||
if (($lnk['rel'] ?? '') === 'action_url') {
|
||||
$href = $lnk['href'];
|
||||
if (!$partnerReferralId) {
|
||||
$u = parse_url($href);
|
||||
if (!empty($u['query'])) {
|
||||
parse_str($u['query'], $q);
|
||||
if (!empty($q['partner_referral_id'])) {
|
||||
$partnerReferralId = $q['partner_referral_id'];
|
||||
} elseif (!empty($q['referral_id'])) {
|
||||
$partnerReferralId = $q['referral_id'];
|
||||
} elseif (!empty($q['referralToken'])) {
|
||||
$referralToken = $q['referralToken'];
|
||||
$partnerReferralId = $partnerReferralId ?? $referralToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$partnerReferralId) {
|
||||
foreach ($links as $lnk) {
|
||||
if (($lnk['rel'] ?? '') === 'self' && !empty($lnk['href'])) {
|
||||
$parts = explode('/partner-referrals/', $lnk['href']);
|
||||
if (count($parts) === 2 && $parts[1]) {
|
||||
$partnerReferralId = $parts[1];
|
||||
$referralToken = $referralToken ?? $partnerReferralId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$href) {
|
||||
http_response_code(500);
|
||||
echo 'Did not receive onboarding URL';
|
||||
exit;
|
||||
}
|
||||
|
||||
$pending = $storage->get('pending_onboarding', []);
|
||||
$pending[$trackingId] = [
|
||||
'school' => $schoolName,
|
||||
'created_at' => time(),
|
||||
'partner_referral_id' => $partnerReferralId,
|
||||
'referral_token' => $referralToken,
|
||||
];
|
||||
$storage->set('pending_onboarding', $pending);
|
||||
|
||||
if ($partnerReferralId && strpos($href, 'partner_referral_id') === false) {
|
||||
$sep = (strpos($href, '?') === false) ? '?' : '&';
|
||||
$href .= $sep . 'partner_referral_id=' . urlencode($partnerReferralId);
|
||||
}
|
||||
|
||||
header('Location: ' . $href);
|
||||
exit;
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
$config = require __DIR__ . '/config.php';
|
||||
require __DIR__ . '/lib/PayPalClient.php';
|
||||
require __DIR__ . '/lib/Storage.php';
|
||||
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
try {
|
||||
$client = new PayPalClient($config);
|
||||
$storage = new Storage($config['storage_path']);
|
||||
|
||||
echo "Partner diagnostics\n\n";
|
||||
echo "Env: " . $config['env'] . "\n";
|
||||
echo "Base API: " . (new PayPalClient($config))->getBaseApi() . "\n";
|
||||
echo "Partner Merchant ID (Payer ID): " . $config['partner_merchant_id'] . "\n\n";
|
||||
|
||||
// Try a simple pull of merchant-integrations
|
||||
$path = '/v1/customer/partners/' . urlencode($config['partner_merchant_id']) . '/merchant-integrations?page_size=20';
|
||||
$resp = $client->api('GET', $path);
|
||||
echo "GET $path => status: " . $resp['status'] . "\n";
|
||||
if ($resp['status'] >= 200 && $resp['status'] < 300) {
|
||||
$list = $resp['body']['merchant_integrations'] ?? [];
|
||||
echo "Integrations returned: " . count($list) . "\n";
|
||||
foreach ($list as $i => $mi) {
|
||||
$mid = $mi['merchant_id'] ?? 'n/a';
|
||||
$tid = $mi['tracking_id'] ?? 'n/a';
|
||||
$rid = $mi['partner_referral_id'] ?? ($mi['referral_id'] ?? 'n/a');
|
||||
$onb = $mi['onboarding_status'] ?? null;
|
||||
echo sprintf("%02d. merchant_id=%s tracking_id=%s referral_id=%s status=%s\n", $i+1, $mid, $tid, $rid, is_array($onb)? json_encode($onb): (string)$onb);
|
||||
}
|
||||
} else {
|
||||
echo json_encode($resp['body'], JSON_PRETTY_PRINT) . "\n";
|
||||
}
|
||||
|
||||
echo "\nRecent sync_debug entries:\n";
|
||||
$diag = $storage->get('sync_debug', []);
|
||||
$tail = array_slice($diag, -5);
|
||||
foreach ($tail as $row) {
|
||||
echo json_encode($row, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE) . "\n";
|
||||
}
|
||||
|
||||
echo "\nPending entries:\n";
|
||||
$pending = $storage->get('pending_onboarding', []);
|
||||
foreach ($pending as $track => $row) {
|
||||
echo $track . ' => ' . json_encode($row, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE) . "\n";
|
||||
}
|
||||
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo 'Diagnostics error: ' . $e->getMessage();
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
$config = require __DIR__ . '/config.php';
|
||||
require __DIR__ . '/lib/PayPalClient.php';
|
||||
require __DIR__ . '/lib/Storage.php';
|
||||
|
||||
$school = $_GET['school'] ?? '';
|
||||
$track = $_GET['track'] ?? '';
|
||||
$pref = $_GET['partner_referral_id'] ?? null;
|
||||
|
||||
if (!$school || !$track) {
|
||||
header('Location: ' . rtrim($config['base_url'], '/') . '/index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$client = new PayPalClient($config);
|
||||
$storage = new Storage($config['storage_path']);
|
||||
|
||||
// First, PayPal may append merchantId to the return URL on successful onboarding
|
||||
$merchantId = $_GET['merchantId']
|
||||
?? $_GET['merchant_id']
|
||||
?? $_GET['merchantIdInPayPal']
|
||||
?? null;
|
||||
|
||||
// If not provided, use Partner Lookup API to retrieve merchant info by tracking_id.
|
||||
// Docs: GET /v1/customer/partners/<partner_merchant_id>/merchant-integrations?tracking_id=
|
||||
if (!$merchantId) {
|
||||
$merchantId = null;
|
||||
$lastResp = null;
|
||||
// poll up to ~15s total
|
||||
for ($i = 0; $i < 15; $i++) {
|
||||
$path = '/v1/customer/partners/' . urlencode($config['partner_merchant_id']) . '/merchant-integrations?tracking_id=' . urlencode($track);
|
||||
$resp = $client->api('GET', $path);
|
||||
$lastResp = $resp;
|
||||
if ($resp['status'] >= 200 && $resp['status'] < 300) {
|
||||
if (!empty($resp['body']['merchant_id'])) {
|
||||
$foundId = $resp['body']['merchant_id'];
|
||||
// CRITICAL: Don't accept the partner's own ID as merchant ID
|
||||
// This means the merchant hasn't completed onboarding yet
|
||||
if (strcasecmp($foundId, $config['partner_merchant_id']) !== 0) {
|
||||
$merchantId = $foundId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
usleep(1000000);
|
||||
}
|
||||
if (!$merchantId) {
|
||||
$debug = $storage->get('return_debug', []);
|
||||
$debug[] = [
|
||||
'ts' => time(),
|
||||
'query' => $_GET,
|
||||
'last_status' => $lastResp['status'] ?? null,
|
||||
'last_body' => $lastResp['body'] ?? null,
|
||||
'track' => $track,
|
||||
'school' => $school,
|
||||
'reason' => 'Merchant onboarding not completed or partner ID returned'
|
||||
];
|
||||
$storage->set('return_debug', $debug);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback by partner_referral_id if available and still missing
|
||||
if (!$merchantId) {
|
||||
// Load pending entry for additional clues
|
||||
$pending = $storage->get('pending_onboarding', []);
|
||||
$entry = $pending[$track] ?? [];
|
||||
$prefFromStore = $entry['partner_referral_id'] ?? $entry['referral_token'] ?? null;
|
||||
$pref = $pref ?: $prefFromStore;
|
||||
}
|
||||
|
||||
if (!$merchantId && $pref) {
|
||||
// Try list integrations and filter by partner_referral_id
|
||||
$path = '/v1/customer/partners/' . urlencode($config['partner_merchant_id']) . '/merchant-integrations?onboarded=true';
|
||||
$resp = $client->api('GET', $path);
|
||||
if ($resp['status'] >= 200 && $resp['status'] < 300 && !empty($resp['body']['merchant_integrations'])) {
|
||||
foreach ($resp['body']['merchant_integrations'] as $mi) {
|
||||
$rid = $mi['partner_referral_id'] ?? ($mi['referral_id'] ?? null);
|
||||
$foundId = $mi['merchant_id'] ?? null;
|
||||
// Only accept if it's a different merchant ID (not the partner's own)
|
||||
if ($rid && strcasecmp($rid, $pref) === 0 && $foundId && strcasecmp($foundId, $config['partner_merchant_id']) !== 0) {
|
||||
$merchantId = $foundId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If still not found, but the list contains exactly one new integration with our tracking id, take it
|
||||
if (!$merchantId) {
|
||||
foreach ($resp['body']['merchant_integrations'] as $mi) {
|
||||
$foundId = $mi['merchant_id'] ?? null;
|
||||
if (!empty($mi['tracking_id']) && strcasecmp($mi['tracking_id'], $track) === 0 && $foundId && strcasecmp($foundId, $config['partner_merchant_id']) !== 0) {
|
||||
$merchantId = $foundId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// As a last resort, if merchants.json already contains the school mapping, use it
|
||||
if (!$merchantId) {
|
||||
$merchants = $storage->get('merchants', []);
|
||||
if (!empty($merchants[$school]['merchant_id'])) {
|
||||
$existingId = $merchants[$school]['merchant_id'];
|
||||
// Only use if it's not the partner's own ID
|
||||
if (strcasecmp($existingId, $config['partner_merchant_id']) !== 0) {
|
||||
$merchantId = $existingId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$merchantId) {
|
||||
// Mark as incomplete onboarding
|
||||
$pending = $storage->get('pending_onboarding', []);
|
||||
if (isset($pending[$track])) {
|
||||
$pending[$track]['status'] = 'incomplete_onboarding';
|
||||
$pending[$track]['last_return_at'] = time();
|
||||
$pending[$track]['note'] = 'Merchant did not complete PayPal onboarding. Partner ID was returned instead.';
|
||||
$storage->set('pending_onboarding', $pending);
|
||||
}
|
||||
|
||||
// Not available yet; merchant needs to complete onboarding
|
||||
header('Location: ' . rtrim($config['base_url'], '/') . '/index.php?onboard=incomplete');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Persist school -> merchant mapping
|
||||
$merchants = $storage->get('merchants', []);
|
||||
$merchants[$school] = [
|
||||
'merchant_id' => $merchantId,
|
||||
'tracking_id' => $track,
|
||||
'connected_at' => time(),
|
||||
];
|
||||
$storage->set('merchants', $merchants);
|
||||
|
||||
// Also annotate the matching pending record so it's visible where you looked
|
||||
$pending = $storage->get('pending_onboarding', []);
|
||||
if (!isset($pending[$track])) {
|
||||
// if for some reason it's missing, recreate a minimal record
|
||||
$pending[$track] = [
|
||||
'school' => $school,
|
||||
'created_at' => time(),
|
||||
];
|
||||
}
|
||||
$pending[$track]['merchant_id'] = $merchantId;
|
||||
$pending[$track]['status'] = 'connected';
|
||||
$pending[$track]['connected_at'] = time();
|
||||
// Store the raw return query for troubleshooting
|
||||
$pending[$track]['last_return_query'] = $_GET;
|
||||
$storage->set('pending_onboarding', $pending);
|
||||
|
||||
header('Location: ' . rtrim($config['base_url'], '/') . '/index.php?onboard=ok');
|
||||
exit;
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
$config = require __DIR__ . '/config.php';
|
||||
require __DIR__ . '/lib/PayPalClient.php';
|
||||
require __DIR__ . '/lib/Storage.php';
|
||||
|
||||
try {
|
||||
$client = new PayPalClient($config);
|
||||
$storage = new Storage($config['storage_path']);
|
||||
|
||||
$pending = $storage->get('pending_onboarding', []);
|
||||
$merchants = $storage->get('merchants', []);
|
||||
$syncDebug = $storage->get('sync_debug', []);
|
||||
$updated = 0;
|
||||
|
||||
foreach ($pending as $track => &$entry) {
|
||||
if (!is_array($entry)) continue;
|
||||
$school = $entry['school'] ?? null;
|
||||
if (!$school) continue;
|
||||
if (!empty($entry['merchant_id'])) continue; // already resolved
|
||||
|
||||
$merchantId = null;
|
||||
|
||||
// Try by tracking_id
|
||||
$path = '/v1/customer/partners/' . urlencode($config['partner_merchant_id']) . '/merchant-integrations?tracking_id=' . urlencode($track);
|
||||
$resp = $client->api('GET', $path);
|
||||
$syncDebug[] = ['ts' => time(), 'track' => $track, 'school' => $school, 'attempt' => 'by-tracking', 'status' => $resp['status'] ?? null, 'body' => $resp['body'] ?? null];
|
||||
if ($resp['status'] >= 200 && $resp['status'] < 300) {
|
||||
if (!empty($resp['body']['merchant_id'])) {
|
||||
$foundId = $resp['body']['merchant_id'];
|
||||
// Don't accept the partner's own ID - means onboarding not completed
|
||||
if (strcasecmp($foundId, $config['partner_merchant_id']) !== 0) {
|
||||
$merchantId = $foundId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try by partner_referral_id if available
|
||||
$ridCandidate = $entry['partner_referral_id'] ?? ($entry['referral_token'] ?? null);
|
||||
if (!$merchantId && $ridCandidate) {
|
||||
$path = '/v1/customer/partners/' . urlencode($config['partner_merchant_id']) . '/merchant-integrations?onboarded=true';
|
||||
$resp2 = $client->api('GET', $path);
|
||||
$syncDebug[] = ['ts' => time(), 'track' => $track, 'school' => $school, 'attempt' => 'list-onboarded', 'status' => $resp2['status'] ?? null, 'body_count' => isset($resp2['body']['merchant_integrations']) ? count($resp2['body']['merchant_integrations']) : null];
|
||||
if ($resp2['status'] >= 200 && $resp2['status'] < 300 && !empty($resp2['body']['merchant_integrations'])) {
|
||||
foreach ($resp2['body']['merchant_integrations'] as $mi) {
|
||||
$rid = $mi['partner_referral_id'] ?? ($mi['referral_id'] ?? null);
|
||||
$foundId = $mi['merchant_id'] ?? null;
|
||||
// Only accept if it's not the partner's own ID
|
||||
if ($rid && strcasecmp($rid, $ridCandidate) === 0 && $foundId && strcasecmp($foundId, $config['partner_merchant_id']) !== 0) {
|
||||
$merchantId = $foundId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback: list without onboarded filter and match by tracking_id/referral
|
||||
if (!$merchantId) {
|
||||
$path = '/v1/customer/partners/' . urlencode($config['partner_merchant_id']) . '/merchant-integrations?page_size=200';
|
||||
$resp3 = $client->api('GET', $path);
|
||||
$syncDebug[] = ['ts' => time(), 'track' => $track, 'school' => $school, 'attempt' => 'list-all', 'status' => $resp3['status'] ?? null, 'body_count' => isset($resp3['body']['merchant_integrations']) ? count($resp3['body']['merchant_integrations']) : null];
|
||||
if ($resp3['status'] >= 200 && $resp3['status'] < 300 && !empty($resp3['body']['merchant_integrations'])) {
|
||||
foreach ($resp3['body']['merchant_integrations'] as $mi) {
|
||||
$tid = $mi['tracking_id'] ?? null;
|
||||
$rid = $mi['partner_referral_id'] ?? ($mi['referral_id'] ?? null);
|
||||
$foundId = $mi['merchant_id'] ?? null;
|
||||
// Only accept if it's not the partner's own ID
|
||||
if (((!empty($tid) && strcasecmp($tid, $track) === 0) || ($ridCandidate && $rid && strcasecmp($rid, $ridCandidate) === 0)) && $foundId && strcasecmp($foundId, $config['partner_merchant_id']) !== 0) {
|
||||
$merchantId = $foundId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try reading the partner referral directly (may include status/links); capture merchant_id if present
|
||||
if (!$merchantId && $ridCandidate) {
|
||||
$path = '/v2/customer/partner-referrals/' . urlencode($ridCandidate);
|
||||
$resp4 = $client->api('GET', $path);
|
||||
$syncDebug[] = ['ts' => time(), 'track' => $track, 'school' => $school, 'attempt' => 'referral-get', 'status' => $resp4['status'] ?? null, 'body' => $resp4['body'] ?? null];
|
||||
if ($resp4['status'] >= 200 && $resp4['status'] < 300 && !empty($resp4['body'])) {
|
||||
// naive scan for a merchant_id field anywhere in response
|
||||
$stack = [$resp4['body']];
|
||||
while (!$merchantId && $stack) {
|
||||
$node = array_pop($stack);
|
||||
if (is_array($node)) {
|
||||
if (isset($node['merchant_id']) && $node['merchant_id']) {
|
||||
$merchantId = $node['merchant_id'];
|
||||
break;
|
||||
}
|
||||
foreach ($node as $v) { if (is_array($v)) { $stack[] = $v; } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($merchantId) {
|
||||
$entry['merchant_id'] = $merchantId;
|
||||
$entry['status'] = 'connected';
|
||||
$entry['connected_at'] = time();
|
||||
$merchants[$school] = [
|
||||
'merchant_id' => $merchantId,
|
||||
'tracking_id' => $track,
|
||||
'connected_at' => $entry['connected_at'],
|
||||
];
|
||||
$updated++;
|
||||
} else {
|
||||
// Mark as incomplete if no merchant ID found
|
||||
$entry['status'] = 'incomplete_onboarding';
|
||||
$entry['last_sync_at'] = time();
|
||||
$entry['note'] = 'Merchant has not completed PayPal onboarding. Only partner ID is available.';
|
||||
}
|
||||
}
|
||||
unset($entry);
|
||||
|
||||
$storage->set('pending_onboarding', $pending);
|
||||
$storage->set('merchants', $merchants);
|
||||
$storage->set('sync_debug', $syncDebug);
|
||||
|
||||
header('Location: ' . rtrim($config['base_url'], '/') . '/index.php?sync=' . $updated);
|
||||
exit;
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo 'Sync failed: ' . htmlspecialchars($e->getMessage());
|
||||
}
|
||||
Reference in new issue
Block a user