# Technical documentation — jumpstarter_crowdfunding

Evidence date: 2026-10-11. Source revision: `39ef15a` on `master` (`39ef15abade9ea4ecf7ccca1c7412392ceb72f18`).

Secret values are named by constant or file and are not copied.

## Stack

| Item | Value | Evidence |
|---|---|---|
| Application | WordPress 4.9.14 | `wp-includes/version.php` `$wp_version` |
| DB schema revision bundled with core | `$wp_db_version` 38590 | same file |
| PHP required by this core | 5.2.4 | `$required_php_version` |
| MySQL required by this core | 5.0 | `$required_mysql_version` |
| Language files in git | `en_GB` (core admin, network, and `en_GB.po`) | `wp-content/languages/` |
| `$wp_local_package` | Not set in `version.php` | same file |
| Themes | Fundify 2.0.14 (IgnitionDeck, `http://fundifytheme.com`); child "Fundify Child" version string `2.0.14.1537361961`, template `fundify`, updated 2018-09-19 | `style.css` headers |
| Custom plugins | PHP 5-style WordPress plugins, no Composer | `so-snapscan`, `img-upload`, `idjs` |
| Tests | None | No PHPUnit, Jest, or CI config |
| Package manifests | None | No `composer.json`, `package.json`, `Dockerfile`, or `bitbucket-pipelines.yml` |

Runtime PHP, MySQL, and the web server are **Unknown / Requires Confirmation**. The child footer calls `money_format()`, which PHP 8 removed.

## Repository shape

The init commit `b99d915` (2020-05-20, Kagiso Malepe) added the WordPress core tree, plugins, and themes. Later commits touch only the files listed in `features.md`.

Root files include the usual WordPress bootstrap (`index.php`, `wp-load.php`, `wp-login.php`, `xmlrpc.php`, and the rest), `readme.html` (stock WordPress readme), `README.md` (two-line product description), `robots.txt`, `.htaccess`, `.gitignore`, and `wordfence-waf.php`.

Not in git, and gitignored: `wp-config.php`, `wp-content/uploads/`, `wp-content/debug.log`.

`wp-content/wflogs/` is listed in `.gitignore` and is still tracked (config PHP files). Gitignore does not untrack files that were committed.

Tracked directories that are stubs or generated, not product source:

| Path | What is there |
|---|---|
| `wp-content/ai1wm-backups/` | Deny stubs (`.htaccess`, `index.php`, `index.html`, `web.config`) |
| `wp-content/backup-db/` | Empty `index.php` |
| `wp-content/updraft/` | Deny stubs |
| `wp-content/cache/` | 55 files committed. Not treated as requirements |
| `wp-content/wflogs/` | Wordfence config PHP |
| `wp-content/wordpress-seo/` | A Yoast tree outside `plugins/`. Nothing in the custom code includes it |

## Application structure

```text
wp-admin/                         WordPress admin, plus committed test.php
wp-includes/                      WordPress 4.9.14
wp-content/themes/fundify/        Parent theme, edited for PayGate and project layout
wp-content/themes/fundify-child/  Header, footer, functions, Home Custom, style.css
wp-content/plugins/ignitiondeck/
wp-content/plugins/ignitiondeck-crowdfunding/
wp-content/plugins/idcommerce/
wp-content/plugins/idsocial/
wp-content/plugins/so-snapscan/   Overdrive SnapScan, export, admin QR
wp-content/plugins/idjs/          Level image fields
wp-content/plugins/img-upload/    Upload REST route
wp-content/plugins/               Other vendored plugins (see features.md)
```

There is no `mu-plugins` directory.

WordPress loads a theme only if it is the active stylesheet. It loads a plugin only if that plugin is active. Both facts are database state and are **Unknown**.

## Custom request paths

| Path or query | Method | Handler | Auth in the handler |
|---|---|---|---|
| Project permalink with `purchaseform=fundify` | GET | `single-ignition_product.php` includes the PayGate partial | PayGate form only if logged in. Register POST is unauthenticated |
| PayGate browser return | GET or POST onto the page that uses `update.php` | `update.php` | Checksum. No WordPress capability check |
| `?export_project_backers={post id}` | GET, any front-end URL, on `init` | `export_project_backers` | Author or `roles[0] === administrator` |
| `/wp-json/so-snapscan/v1/sync/` | POST | `sync_snapscan_mdid_orders` | None |
| `/wp-json/so-snapscan/v1/download/` | POST | `download_image` (returns a QR URL) | None |
| `/wp-json/img-upload/v1/upload/` | POST | `upload_image` | Commented-out `is_user_logged_in` check. Not active |
| `wp-admin` → Settings → SnapScan | GET | `so_snapscan_settings_page` | `manage_options` |
| `wp-admin/test.php` | GET | Standalone script | Loads `wp-config.php`. No capability check in the file |

REST routes were registered with `register_rest_route` without `permission_callback`. WordPress 4.9 does not require that argument (it arrived in 5.5). The callbacks themselves do not call `current_user_can` or `is_user_logged_in`.

## Data

Custom code reads and writes these tables. Prefix in plugin code is `$wpdb->prefix`. Two theme queries hardcode `wp_`.

| Table | Written by | Read by |
|---|---|---|
| `{prefix}ign_pay_info` | PayGate `update.php`; SnapScan sync | Sync duplicate check; CSV export |
| `wp_ign_pay_info` (literal) | Not written under that literal name | Project backer count; child footer sum |
| `{prefix}memberdeck_orders` | PayGate return (`level_id` 1); SnapScan sync (`level_id` 0) | Not read by Overdrive code |
| `{prefix}mdid_orders` | Both payment recorders | Not read by Overdrive code |
| `{prefix}posts` | WordPress | Export looks up `post_author` with an interpolated post id |
| `{prefix}postmeta` | Reward upload (`ign_project_level_image_{n}`); IgnitionDeck owns `ign_project_id` | Project pages, QR, export, PayGate return |

Post meta keys used by Overdrive code:

| Key | Use |
|---|---|
| `ign_project_id` | IgnitionDeck product id for a post |
| `ign_fund_end` | Read on the admin QR page. Not displayed |
| `ign_project_level_image_{n}` | Reward image URL |
| `ign_product_video`, `ign_project_long_description`, `ign_project_type` | Read by the parent single template |

User meta `md_shipping_info` is read by the CSV export and is not written by Overdrive code.

PHP session keys written by the purchase partial: `address`, `country`, `state`, `city`, `zip`, `user_purchase_request`, `project_url`. The PayGate return reads the five shipping keys and does not call `session_start()`.

No migration files exist. IgnitionDeck creates its tables on plugin activation (vendor `memberdeck_install` and the crowdfunding installer). That SQL was not copied here.

## Integrations

### PayGate PayWeb v2

- Endpoint: `https://www.paygate.co.za/paywebv2/process.trans`
- Browser POST fields: `PAYGATE_ID`, `REFERENCE`, `AMOUNT`, `CURRENCY`, `RETURN_URL`, `TRANSACTION_DATE`, `CHECKSUM`
- Checksum: MD5 of those values joined with `|`, plus the secret stored in the theme
- Copies of the secret and the merchant id: `project/project-purchase-form.php`, `project/project-purchase-form_.php`, `XXpurchase.php`, `update.php`
- A second merchant id is commented out in the purchase templates
- Return fields consumed: `TRANSACTION_STATUS`, `RESULT_CODE`, `AUTH_CODE`, `AMOUNT`, `TRANSACTION_ID`, `CHECKSUM`, `REFERENCE`, and the other PayGate fields listed in the PRD

### SnapScan

- Host: `https://pos.snapscan.io/`
- QR: `qr/{snapcode}.png` and `qr/{snapcode}` with query `id`, `project`, `snap_code_size=125`
- Payments: `GET merchant/api/v1/payments` with basic auth `API_KEY` / `TOKEN`
- Constants in `so-snapscan.php`: `SNAPSCAN_API_URL`, `SNAPSCAN_SNAPCODE`, `API_KEY`, `TOKEN`
- The settings validator, if it were used, accepts only a 32-character hex `api_key` and would clear any other value. The live constant is not loaded from that option. The save button is commented out.

### Other outbound tags and plugins

- Child header loads `https://www.googletagmanager.com/gtag/js?id=UA-55077943-1` and initialises `gtag`. It does not call `gtag('config', ...)`. Provenance: **Implemented**.
- Vendored and inactive-until-enabled: Google Tag Manager plugin, Site Kit, Facebook Pixel, Yoast, Wordfence, ManageWP Worker, CleanTalk, reCAPTCHA, Contact Form 7, Icegram, Really Simple SSL.
- IgnitionDeck Commerce can call PayPal Adaptive, Stripe, Authorize.Net, and Coinbase using libraries under `idcommerce/lib/`. Configuration is not in git.

## Host and web server

`wordfence-waf.php` includes `/usr/www/users/jumpszecsx/wp-content/plugins/wordfence/waf/bootstrap.php` when that file exists, and sets `WFWAF_LOG_PATH` to `/usr/www/users/jumpszecsx/wp-content/wflogs/`. The comment in the file says not to remove it while `auto_prepend_file` points at it. Whether PHP's `auto_prepend_file` is set that way is **Unknown**.

`.htaccess` denies direct access to `wp-content/debug.log` and then contains the standard WordPress rewrite block with `RewriteBase /`.

`robots.txt` disallows `/wp-admin/` and allows `/wp-admin/admin-ajax.php`.

No deploy script, git-ftp config, or pipeline file is in the revision. How the tree reaches `/usr/www/users/jumpszecsx/` is **Unknown**.

## Auth

- WordPress user table and cookies. Purchase registration uses `wp_insert_user` and `wp_setcookie` (legacy helper).
- PayGate: MD5 checksum shared secret in the theme.
- SnapScan: HTTP basic auth with constants.
- No OAuth client, no application password integration, and no custom login plugin in the Overdrive code.
- White Label CMS is vendored. Its effect on the login screen is **Unknown**.

## Cron and background work

| Schedule | Hook | Where |
|---|---|---|
| `five_seconds` (interval 5) | none | Registered only, in `so-snapscan.php` |
| IgnitionDeck twice daily and hourly | `schedule_twicedaily_idf_cron`, `schedule_hourly_id_cron` | Vendor cron files |
| Wordfence daily and hourly | vendor | `wordfence` |
| ManageWP Worker | vendor | `worker/init.php` |
| WordPress core update checks | vendor | core |

SnapScan import runs when a browser executes `so-snapscan.js` on a page that has `[data-projectid]`. It is not a queued job.

## Security findings in the tree

These are observations for human review. They are not an invitation to use the behaviour. Values of secrets are intentionally absent.

| Finding | Where | Why it matters |
|---|---|---|
| SnapScan API key, token, and snap code are PHP constants | `so-snapscan.php` | Anyone with the repository has the merchant API credentials. The admin UI cannot rotate them |
| PayGate merchant id and checksum secret are hardcoded, including in backup copies of the templates | Purchase templates and `update.php` | The checksum secret is the merchant's PayWeb key |
| Sync, QR, and image upload routes do not authenticate | REST registrations in `so-snapscan.php` and `img-upload.php` | The upload writes attachments and post meta. Sync inserts orders. Download returns a QR URL |
| Sync SQL builds `product_id="{id}"` by concatenation | `sync_snapscan_mdid_orders` | The export query and the PayGate post lookup use different styles. The export uses `$wpdb->prepare`. The post lookup in `update.php` interpolates `$pieces[0]` |
| Project backer count interpolates `$project_id` into SQL | `project/project-details.php` | Same class of issue |
| `wp-admin/test.php` enables `display_errors`, loads `wp-config.php`, and prints post meta. Commented statements name database `jumpstarter_wp` and contain updates to funding meta | `wp-admin/test.php` | Debug file shipped in `wp-admin` |
| Child header contains a hidden block of outbound links that are unrelated to crowdfunding | `fundify-child/header.php` | Consistent with a injected spam block. Confirm whether the child theme is active and remove the block if it is not intentional |
| CSV `readfile` uses the uploads URL | `export_project_backers` | The file is written to the uploads directory and then read back via HTTP |
| Wordfence log config is committed | `wp-content/wflogs/` | May contain site-specific paths. Not reproduced here |
| Registration form has no nonce and no captcha in the template | `project-purchase-form.php` | reCAPTCHA plugin may add some if active. The template itself does not |

## Development and testing

- History is twelve commits on `master` from 2020-05-20 to 2023-08-04. Authors: Kagiso Malepe (`kagiso@overdrive.co.za`), then one commit by Joshua G-D (`joshua@overdrive.co.za`).
- There is no test suite, linter config, or CI workflow.
- `wp-admin/test.php` and Query Monitor are the only development aids in the tree.
- Theme backup copies (`*.bckDDMMYYYY`, `*copy*`, `XXpurchase.php`) are historical edits kept in the tree. They are easy to confuse with the live template. The single template that WordPress will use for an `ignition_product` is `single-ignition_product.php` in the active theme. The child theme does not provide a current single template, only `single-ignition_product.php.bck`.

## Configuration not in git

| Item | Status |
|---|---|
| `wp-config.php` | Gitignored, absent |
| Database name, user, password, host, salts, `$table_prefix` | **Unknown**. Comment in `test.php` says `jumpstarter_wp` |
| Active theme and active plugins | **Unknown** |
| IgnitionDeck licence, gateway toggles, permalink structure | **Unknown** |
| SnapScan and PayGate secrets | Present in source, omitted here |
| `auto_prepend_file` for Wordfence | **Unknown**. The bootstrap file expects it |
| Sonar project file and `sonarqube_scan.bat` | Not in this revision. Named only by the 2026-09-28 onboarding scan |
