My Supply Chain Emissions
“My Supply Chain Emissions” lets a CC Partner company upload its supplier list with annual spend, and see estimated Scope 3 emissions for each supplier — using the supplier’s own reported figures where a share exists, and a spend-based EEIO estimate from the supplier’s SIC code otherwise.
Why it exists
Section titled “Why it exists”Most of a large organisation’s footprint is in its supply chain, and almost none of its suppliers have a carbon report. The compromise: estimate from spend using published emissions intensity per £1m of turnover by industry (SIC code), and progressively replace those estimates with real reported numbers as suppliers join the School and share their reports. The “data_source” column on every row exists so a buyer can see which of the two they are looking at.
Entry points
Section titled “Entry points”| Path | What it does |
|---|---|
wp-content/themes/carboncalculator/page-my-supply-chain-emissions.php | The page, /my-supply-chain-emissions/ |
wp-content/themes/carboncalculator/partials/components/supplier-table.php | Table markup |
wp-content/themes/carboncalculator/assets/js/supplier-table.js | 2,786 lines — sorting, filtering, inline edit, bulk actions |
wp-content/themes/carboncalculator/assets/js/upload-suppliers.js | CSV parse + batched upload from the browser |
wp-content/themes/carboncalculator/functions.php:2628 | GET /carboncalculator/v1/suppliers |
wp-content/themes/carboncalculator/functions.php:3001 | POST /carboncalculator/v1/suppliers/upload-batch |
wp-content/themes/carboncalculator/functions.php:4262 | GET /suppliers/dashboard-summary |
wp-content/themes/carboncalculator/functions.php:2186 | GET /suppliers/filter-options |
wp-content/themes/carboncalculator/functions.php:2445 | POST /suppliers/bulk |
wp-content/themes/carboncalculator/functions.php:3912 | carboncalculator_hydrate_supplier_join_row() — the emissions rules |
Also GET /suppliers/detail, GET /suppliers/join/years,
POST /suppliers/join/update, POST /suppliers/join/delete
(registered at functions.php:462–:500).
How it works
Section titled “How it works”Data model
Section titled “Data model”cc_company_supplier is a join table on the Carbon blog, one row per
(buyer company, supplier company, reporting year) —
UNIQUE KEY buyer_supplier_year. Columns: supplier_name,
contact_email, annual_spend, emissions, intensity,
report_status, sic_code, sic_code_conversion, is_target_supplier,
updated_at (DDL at plugins/carboncalc/includes/Migrations.php:646).
is_target_supplier (schema v21) distinguishes the buyer’s declared target
supply-chain list from other suppliers picked up along the way. updated_at
came in v22.
cc_sic_codes holds the SIC lookup: sic_code, description, and
year_values as JSON so one row carries a different intensity per
reporting year.
Upload
Section titled “Upload”carboncalculator_upload_suppliers_batch() (functions.php:3001). The
browser parses the CSV and POSTs it in batches of rows; the server
processes each row:
carboncalculator_require_supply_chain_edit_permission()— role gate.- Requires a valid
reporting_year_id. - Required CSV fields:
company_name,crn,annual_spend. - Normalise the CRN (
carboncalculator_normalize_supplier_crn(),:3655). - Resolve the supplier to a
cc_companyrow, in this order:- existing row by CRN,
- Moodle CRM (
MoodleCompany::lookup($crn)) → insert a full company row includingprofile_jsonandis_partner, - Companies House (
CompaniesHouse::lookup($crn)) → insert a minimal row with name, registered address and the raw profile.
- Resolve the SIC code — from Companies House if the CRM had none.
- Resolve
contact_emailthroughSupplierContactEmail::resolve_for_upload_row()(plugins/carboncalc/includes/SupplierContactEmail.php:27), which prefers the real School users’ emails over the CSV value when the supplier is an active School company that has accepted Carbon T&Cs. - Upsert the join row.
Per-CRN results are cached for the batch ($companyIdByCrn,
$ch_sic_by_crn, $crm_lookup_by_crn at :3074–:3080) so a CSV listing
the same supplier twice makes one external call.
Everything is logged as JSON lines to wp-content/carboncalc-supplier-upload.log
under a per-request debug_id, plus CRM failures to
wp-content/carboncalc-crm.log.
mark_as_target on the batch marks every row as a target supplier — CSV
uploads set it; manual single-supplier adds send per-row
is_target_supplier instead (comment at :3030).
Emissions rules
Section titled “Emissions rules”carboncalculator_hydrate_supplier_join_row() (:3912) is the single
source of truth, applied identically by the list API, the dashboard summary
and the detail endpoint:
flowchart TD
A[join row] --> B{supplier has active CC account and shared report}
B -->|yes| C[intensity + apportioned emissions from supplier report]
C --> C2[data_source = Supplier reported]
B -->|no| D{stored data_source = Supplier reported}
D -->|yes| E[emissions = intensity x spend / 1m]
D -->|no| F[resolve SIC conversion for the year]
F --> G[EEIO: emissions = intensity x spend / 1m]
G --> G2[data_source = EEIO Estimate]
- EEIO:
emissions_tco2e = round(intensity × (annual_spend / 1_000_000), 2)where intensity is tCO2e per £1m of spend (carboncalculator_supplier_eeio_emissions_tco2e(),:3738). - SIC conversion resolution prefers the value stored on the join row, then
falls back to
cc_sic_codesfor the report’s year (carboncalculator_resolve_supplier_sic_conversion(),:3714). - EEIO values only backfill — they are applied when the stored value is
empty, which
carboncalculator_supplier_metric_is_empty()(:3699) defines as null, blank, non-numeric, or<= 0. - Supplier-reported figures come from a share:
carboncalculator_load_shared_supplier_metrics_for_buyer()(:3814) reads the shared report’s total and turnover, andcarboncalculator_supplier_reported_metrics()(:1355) apportions them to the buyer by the buyer’s share of the supplier’s turnover. data_sourcevalues seen in code:Supplier reported,EEIO Estimate,Error - Invalid CRN.
List, filter, sort
Section titled “List, filter, sort”carboncalculator_get_suppliers() (:2628) paginates and filters in SQL
(carboncalculator_supplier_list_filter_where(), :2031;
carboncalculator_supplier_data_source_filter_where(), :2137), then
hydrates. Because emissions and intensity are computed after the SQL,
sorting on them happens in PHP over the hydrated rows
(carboncalculator_sort_hydrated_supplier_rows(), :2592).
Filters offered: reporting year, target supplier, active CC account, free text search, SIC code, trade category, data source.
Configuration
Section titled “Configuration”COMPANIES_HOUSE_API_KEY— Companies House Company Profile API (plugins/carboncalc/includes/CompaniesHouse.php:47). Absent means every CH lookup logslookup_missing_api_keyand returns null.MOODLE_WS_TOKEN+LMS_URL— the CRM lookup.carboncalc_feature_sic_lookup_indicator— shows SIC lookup provenance in the table.- SIC intensities are data in
cc_sic_codes, managed at wp-admin → Carbon Calculator → SIC Codes.
Filters: carboncalc_supply_chain_editor_roles,
carboncalc_supply_chain_view_roles,
carboncalc_supplier_upload_contact_roles,
carboncalc_company_has_accepted_terms.
Invariants and gotchas
Section titled “Invariants and gotchas”- Visibility requires a CC Partner company.
user_can_view_supply_chain()needscc_company.is_partner = 1, or the roletlactionplans_partner_adminas a bypass (plugins/carboncalc/includes/RolePermissions.php:506). Editing additionally needsscs_cc_adminor a School Admin role — andscs_cc_adminalone is not enough if the company is not a CC Partner (RolePermissions.php:295). - A stored emissions value of
0is treated as missing and gets overwritten by the EEIO estimate. A supplier legitimately reporting zero cannot be recorded as zero. - Sorting by emissions or intensity cannot use the database. Those
columns are derived at hydration time, so sorting is done in PHP after
paging. Check
carboncalculator_sort_hydrated_supplier_rows()before assuming a sort is SQL-backed. - Uploads hit two external APIs per unknown CRN. A large CSV of new suppliers is slow and rate-limit exposed. The per-batch cache only helps within one request.
contact_emailisVARCHAR(255)and can hold several addresses joined by,.SupplierContactEmail::format_contact_emails()truncates at a comma boundary to fit (SupplierContactEmail.php:220).- The theme’s supplier REST routes all use
'permission_callback' => '__return_true'and check permission inside the callback.carboncalculator_get_suppliers()and friends must each call the gate themselves. - Migrations for this table loop over the hard-coded blog list
[6, 8](Migrations.php:505,:535). A Carbon site on any other blog id will not get new columns. - Companies House logs every lookup, including CRNs, to
wp-content/carboncalc-companieshouse.log. These log files are gitignored (*.log) but they do accumulate on the server.
Changing it safely
Section titled “Changing it safely”- New emissions rules belong in
carboncalculator_hydrate_supplier_join_row(), never in a call site. Three endpoints depend on it agreeing with itself; the docblock at:3907says exactly that. - New join-row columns: add DDL in
Migrations::ensure_carbon_site_cc_tables()and a gatedALTER TABLEhelper for existing installs (followmaybe_add_company_supplier_updated_at_column(),Migrations.php:530), bumpCARBONCALC_SCHEMA_VERSION, then surface the column in the hydrator’s return array and insupplier-table.php/supplier-table.js. - New CSV columns: update
$required_fields(:3044) if mandatory, the browser-side header validation inupload-suppliers.js, and the row loop. - New filters need matching work in
carboncalculator_supplier_list_filter_where(),carboncalculator_get_supplier_filter_options()and the JS. - Verify by hand: upload a two-row CSV with one known-School supplier and
one CRN that only exists at Companies House. Confirm one row reads
“Supplier reported” and the other “EEIO Estimate”, then tail
wp-content/carboncalc-supplier-upload.logfor the decision trail. - Deliberately not abstracted: the inline closures inside
carboncalculator_upload_suppliers_batch()($insert_company_from_crm,$insert_company_from_companies_house, the loggers). They close over the batch caches; pulling them out means threading state.
None. carboncalculator_supplier_eeio_emissions_tco2e() and
carboncalculator_hydrate_supplier_join_row() are both pure enough to test
and both directly determine client-facing numbers.
Related: [[report-sharing]], [[company-lookup-integrations]], [[carbon-access-gates]], [[company-emissions-dashboard]].