Skip to content

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.

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.

PathWhat it does
wp-content/themes/carboncalculator/page-my-supply-chain-emissions.phpThe page, /my-supply-chain-emissions/
wp-content/themes/carboncalculator/partials/components/supplier-table.phpTable markup
wp-content/themes/carboncalculator/assets/js/supplier-table.js2,786 lines — sorting, filtering, inline edit, bulk actions
wp-content/themes/carboncalculator/assets/js/upload-suppliers.jsCSV parse + batched upload from the browser
wp-content/themes/carboncalculator/functions.php:2628GET /carboncalculator/v1/suppliers
wp-content/themes/carboncalculator/functions.php:3001POST /carboncalculator/v1/suppliers/upload-batch
wp-content/themes/carboncalculator/functions.php:4262GET /suppliers/dashboard-summary
wp-content/themes/carboncalculator/functions.php:2186GET /suppliers/filter-options
wp-content/themes/carboncalculator/functions.php:2445POST /suppliers/bulk
wp-content/themes/carboncalculator/functions.php:3912carboncalculator_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).

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.

carboncalculator_upload_suppliers_batch() (functions.php:3001). The browser parses the CSV and POSTs it in batches of rows; the server processes each row:

  1. carboncalculator_require_supply_chain_edit_permission() — role gate.
  2. Requires a valid reporting_year_id.
  3. Required CSV fields: company_name, crn, annual_spend.
  4. Normalise the CRN (carboncalculator_normalize_supplier_crn(), :3655).
  5. Resolve the supplier to a cc_company row, in this order:
    • existing row by CRN,
    • Moodle CRM (MoodleCompany::lookup($crn)) → insert a full company row including profile_json and is_partner,
    • Companies House (CompaniesHouse::lookup($crn)) → insert a minimal row with name, registered address and the raw profile.
  6. Resolve the SIC code — from Companies House if the CRM had none.
  7. Resolve contact_email through SupplierContactEmail::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.
  8. 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).

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_codes for 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, and carboncalculator_supplier_reported_metrics() (:1355) apportions them to the buyer by the buyer’s share of the supplier’s turnover.
  • data_source values seen in code: Supplier reported, EEIO Estimate, Error - Invalid CRN.

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.

  • COMPANIES_HOUSE_API_KEY — Companies House Company Profile API (plugins/carboncalc/includes/CompaniesHouse.php:47). Absent means every CH lookup logs lookup_missing_api_key and 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.

  • Visibility requires a CC Partner company. user_can_view_supply_chain() needs cc_company.is_partner = 1, or the role tlactionplans_partner_admin as a bypass (plugins/carboncalc/includes/RolePermissions.php:506). Editing additionally needs scs_cc_admin or a School Admin role — and scs_cc_admin alone is not enough if the company is not a CC Partner (RolePermissions.php:295).
  • A stored emissions value of 0 is 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_email is VARCHAR(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.
  • 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 :3907 says exactly that.
  • New join-row columns: add DDL in Migrations::ensure_carbon_site_cc_tables() and a gated ALTER TABLE helper for existing installs (follow maybe_add_company_supplier_updated_at_column(), Migrations.php:530), bump CARBONCALC_SCHEMA_VERSION, then surface the column in the hydrator’s return array and in supplier-table.php / supplier-table.js.
  • New CSV columns: update $required_fields (:3044) if mandatory, the browser-side header validation in upload-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.log for 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]].