mcpField
E
emap-merchant-signup

emap-merchant-signup

github.com/daptondev2开发067 次安装6 周前更新
代码仓库

关于 emap-merchant-signup

用于在合作伙伴网站嵌入 EMAP 商户注册流程的开发技能,提供完整表单、重定向和邮件注册三种集成方式。它指导不同技术栈的实现,并要求完成前进行模式、安全与覆盖检查。

主要功能

  1. 01三种商户注册集成模式
  2. 02技术栈检测与表单交付指引
  3. 03由浏览器直接请求 EMAP
  4. 04模式一致性验证门禁
  5. 05安全与覆盖验证指引

适用场景

  1. 01在合作伙伴网站承载全部六步注册
  2. 02将商户重定向至 EMAP 入驻流程
  3. 03通过邮件链接继续注册
  4. 04在静态或服务端渲染网站嵌入表单

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/daptondev2/emap-skills --skill emap-merchant-signup

安装后在 Claude Code 中输入/emap-merchant-signup确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

AGENTS.md
signup-steps-schema.json
references/
api-errors.md
api-quirks.md
dropdown-fallbacks.json
field-catalog.md
mode-1-fullform.md
mode-2-redirect.md
mode-3-api.md
security-checklist.md
stack-guide.md
templates/integration-1/
plain-html.html
README.md
SignupForm.tsx
templates/integration-2/
plain-html.html
README.md
SignupForm.tsx
templates/integration-3/
plain-html.html
README.md
SignupForm.tsx
verify/
README.md
verify/hooks/
claude-code-settings.json
verify/scripts/
emap_gate.py
verify_form.py

详细介绍

商户注册集成

一份开发者指南,介绍如何在合作伙伴网站中嵌入 EMAP 商户注册表单。 商户在合作伙伴的网站上填写基本业务信息; 之后的所有步骤(公司详情、银行信息、电子签名)均由 EMAP 处理——或者, 使用集成 1 时,合作伙伴自行托管全部 6 个步骤。

目录


开始之前 — 必答问题

停止。开发者回答以下两个问题之前,不要阅读任何参考文件、打开任何模板,也不要生成任何代码。开始时只问这两个问题;仅当步骤 0无法检测到技术栈时,才询问技术栈。

问题 1 — 选择哪种集成方案?

请开发者从以下 3 个选项中选择一个,不得提供其他选项。如果 Agent 有多选一问题工具,请使用该工具,并将粗体文本用作每个选项的标签,其余内容用作说明(标签中不要包含序号)。否则,请以带编号的列表用纯文本提问,并等待回答。

  • 完整表单(集成 1) — 在你的网站上托管完整的 6 步商户注册体验。商户无需离开你的平台,即可完成全部步骤(1–6)。每个步骤都由浏览器直接向 EMAP API 发送 POST 请求。适用于任何技术栈;由商户的浏览器调用 EMAP。
  • 重定向交接(集成 2) — 商户在你的网站上填写单页步骤 1 表单。提交后,他们会直接重定向到 EMAP 的入驻注册流程,完成剩余步骤。适用于任何技术栈;由商户的浏览器调用 EMAP。
  • 基于电子邮件的注册(集成 3) — 商户在你的网站上填写单页步骤 1 表单,该表单会由浏览器直接向 EMAP 发送 POST 请求。EMAP 创建其账户,并通过电子邮件发送安全的续办链接。商户点击链接后,即可从收件箱继续完成完整的入驻流程。适用于任何技术栈;由商户的浏览器调用 EMAP。

在开发者从这三个方案中选定一个之前,不要继续。

问题 2 — 合作伙伴密钥

开发者选定变体后,请像问题 1 一样,让他们从以下 3 个选项中恰好选择一个(如果你的 Agent 有选择题工具,就使用该工具;否则以纯文本形式列出带编号的选项)。每个选项的描述都必须包含以下原样写出的获取说明——开发者会根据这些说明了解在哪里查找或获取密钥。此外,在他们回答前还要告知他们:EMAP 是从商户的浏览器调用的,因此密钥会直接嵌入页面的 JavaScript 中,任何查看页面源代码的人都能看到。 EMAP 仅将其视为归因/推荐值(而非授予任何访问权限的凭证),因此泄露密钥的实际风险是其他网站的注册被错误地归因到此密钥,而不是发生安全漏洞——但开发者在决定前应该了解这一点。

  • 是,我有合作伙伴密钥 — 描述:"登录合作伙伴门户 → Integration → API Integration → 复制此处显示的 API 密钥 → 将其粘贴到这里。"
  • 否,我没有合作伙伴密钥 — 描述:"在 https://emap.easypaydirect.com/signup/partner 注册成为合作伙伴。注册后,前往 Integration → API Integration → 复制合作伙伴密钥 → 返回此处并粘贴。"
  • 跳过(不使用密钥继续) — 描述:"注册仍会正常进行,但不会归因到你的合作伙伴账户。如果你不希望密钥显示在页面源代码中,请选择此项。"

如果开发者选择“是,我有合作伙伴密钥”: 请他们现在粘贴密钥。将其记为 EMAP_PARTNER_KEY——稍后你会将其直接写入生成文件中的 EMAP_PARTNER_KEY 常量。继续执行步骤 0。

如果开发者选择“否,我没有合作伙伴密钥”: 告诉他们按照上述选项描述中的注册链接操作,然后在准备好时返回并粘贴密钥。暂时不使用密钥继续执行步骤 0——如果他们之后粘贴了密钥,则将其写入生成文件中的 EMAP_PARTNER_KEY 常量。

如果开发者选择“跳过(不使用密钥继续)”: 不使用合作伙伴密钥继续执行步骤 0。

只有在回答完这两个问题后,你才能继续前往步骤 0,并开始读取参考文件或生成代码。


参考文件

仅在实现相应功能时加载参考文件,不要一开始就全部读取。

文件在以下情况下阅读……
signup-steps-schema.json构建或验证任何表单时(全部 3 种集成)——这是权威的机器可读来源,包含每个字段的控件 type(select/radio/checkbox-group/text/等)、选项标签(staticDropdowns)、动态下拉列表端点以及各国家/地区的验证规则(countryVariants)。绝不要根据字段名称或 Markdown 表格推断控件类型或硬编码字段的选项标签——始终以此文件为准。
references/field-catalog.md构建集成 2 或 3 时——完整字段列表、类型、约束,以及会触发自动提交的字段。不适用于集成 1——其国家/地区和行业的值格式不同;请改用 mode-1-fullform.md 和上述架构文件。
references/mode-1-fullform.md实现集成 1 时——全部 6 步的 API 契约、字段规则、UUID 生命周期和条件逻辑。此文件仅记录 API 负载/值规则——不说明应渲染哪种 HTML 控件,也不说明哪些验证规则因国家/地区而异;请从 signup-steps-schema.json 获取这些信息。
references/mode-2-redirect.md实现集成 2 时——URL 构造、自动提交逻辑、合作伙伴归因和 UTM 透传
references/mode-3-api.md实现集成 3 时——基于电子邮件的注册 API 契约、请求/响应结构、恢复链接和错误处理
references/api-errors.md处理错误时——所有 HTTP 状态代码、响应结构和建议的开发者操作
references/stack-guide.md步骤 0:在项目所用技术栈中交付表单。涵盖技术栈检测、React / Vue / Angular / Svelte / 服务端渲染 / 静态页面 / 网站构建器 / 移动端交付、原生移植规则,以及绝不能在哪些服务端代码中调用 EMAP
references/security-checklist.md上线前——所有必须通过的安全要求
references/dropdown-fallbacks.json构建任何集成时——所有 EMAP /api/partner/* 下拉列表响应的静态快照,嵌入每个模板的 EMAP_DROPDOWN_FALLBACKS 常量中;当实时调用失败或未返回可用数据时使用。从 EMAP 测试服务器获取;待生产环境提供该 API 后重新采集
references/api-quirks.md构建集成 1 时——与 EMAP 自有注册页面行为不同的 API 行为(税号和注册号码豁免、SSN 国家/地区、推荐来源和行业匹配),以及此文件中的规则原因
verify/README.md编写或编辑任何表单文件后、宣布构建完成前——如何运行 verify_form.py、其 5 类检查涵盖的内容,以及它无法检测的问题

步骤 0:检测项目技术栈

此表单适用于任何技术栈。唯一的架构规则涉及 EMAP 请求的来源,而不是使用哪个框架来渲染页面:

所有 EMAP 调用(所有 POST、/api/partner/* 下拉列表 GET,以及 Integration 2 重定向)都由商户的浏览器发起,绝不由合作伙伴的服务器发起。 EMAP 会根据调用方的 IP 对注册请求进行速率限制和滥用评分。如果通过合作伙伴的服务器代理,每位商户都会共用该服务器的 IP:一个滥用者就会导致该合作伙伴的所有注册请求受到速率限制,而 EMAP 也会失去针对每位商户的垃圾信息判断信号。

网站的其他功能可以使用任何后端。只要 EMAP 请求由页面中的 JavaScript 发起,服务端渲染的页面也没问题。绝不要从以下任一位置调用 EMAP:

  • 路由处理程序、API 路由、Server Actions
  • loaders 或 actions
  • SSR 数据获取
  • 控制器、curl / wp_remote_post、无服务器函数

references/stack-guide.md 按框架列出了需要避免的具体服务端 API。

根据项目文件检测技术栈:package.json 依赖项、composer.json、Gemfile、manage.py、*.csproj、SSG 配置等。技术栈指南中列出了相关信号。仅当信号缺失或相互冲突时,才询问开发者。然后按以下方式交付表单:

项目交付内容
React(Next.js、Vite、Remix / React Router、Gatsby、Astro + React)templates/integration-<n>/SignupForm.tsx。从任意页面渲染它。对于仅含 JS 的项目,将其重命名为 .jsx 并移除类型
Vue / Nuxt、Angular、Svelte / SvelteKit、Solid、其他组件框架在仅客户端的挂载钩子中挂载 plain-html.html 的标记和脚本,方式与 SignupForm.tsx 相同(推荐);或按原生方式进行 1:1 移植
服务端渲染(PHP / WordPress / Laravel、Rails、Django / Flask、ASP.NET、Twig / Liquid / 等)嵌入 plain-html.html:将其 CSS 和脚本作为静态资源文件,并将其 .emap-signup 标记放入页面模板
静态网站 / SSG将 plain-html.html 作为独立页面,或按上述方式嵌入
托管式网站构建器(Webflow、Wix、Squarespace、Shopify、Framer)使用自定义代码嵌入块,或托管 plain-html.html 并通过 iframe 嵌入或链接到该页面
移动应用在 WebView 中加载托管的 plain-html.html

阅读 references/stack-guide.md 了解各技术栈的详细说明:SSR 陷阱、模板引擎转义、CSP、iframe,以及原生移植规则。

两个模板使用的是同一个经过测试的表单。plain-html.html 的 CSS 作用域限定在 .emap-signup 下,其脚本由 IIFE 包装并通过 onReady() 启动,因此可嵌入现有页面,而不会泄漏样式或全局变量。将生成的表单所有文件放在同一个 form_dir 下,该目录中不要放其他内容。verify_form.py 会递归扫描其中所有前端源文件类型。


第 1 步:安装验证门禁

停止。完成此步骤之前,不要编写、编辑或生成任何表单文件,无论是全新项目还是正在继续开发的项目。项目中已有表单代码,并不代表已安装门禁。先检查 .emap/emap_gate.py 是否存在。如果不存在,就执行此步骤,不管项目中已完成了什么。

门禁决定何时构建完成。emap_gate.py 会自行运行 verify_form.py 并记录结果;它绝不会信任你填写的状态。它是纯 Python 3 脚本,因此可在 CI 中、手动运行,也适用于任何能够执行 shell 命令的 Agent。有关脚本检查内容,请参阅验证:架构符合性。

  1. 将 3 个文件逐字节复制到目标项目的 .emap/ 文件夹中:

    .emap/ 是构建工具,不属于表单,因此不要部署它。可以将其提交到版本控制(这样 CI 就能运行门禁)。可以将 .emap/verify-cache.json 加入 gitignore。

  2. 将构建标记为进行中。 开始为选定的集成模式生成代码时,立即写入 .emap/build-state.json:

    { "status": "in_progress", "integration": "1", "form_dir": "merchant-signup" }
    

    根据正在构建的模式,使用 "2" 或 "3"。将 form_dir 设为存放生成表单文件的文件夹,该路径相对于项目根目录。此文件夹中不应包含其他内容。

  3. 可选:让 Agent 自动执行门禁。

    • Claude Code: 将 verify/hooks/claude-code-settings.json 中的 hooks 键合并到目标项目的 .claude/settings.json。如果文件不存在,则创建它。如果 hooks.Stop 已有条目,请追加此条目,切勿覆盖其他条目。这样,Claude Code 就无法结束当前轮次,除非门禁通过,或构建被标记为 blocked 或 cancelled。
    • 其他带有 Agent 完成时运行的 hook 的 Agent: 按该 Agent 的 hook 格式注册 python3 .emap/emap_gate.py。如果没有需要阻止的内容,脚本将以状态码 0 退出;否则会在标准输出中显示检查结果,并以状态码 1 退出。
    • 不支持 hook 的 Agent: 无需安装任何内容。请在宣布构建完成前自行运行门禁(参见验证:架构符合性)。
    • 任何 Agent: 开发者也可以在 CI 或 pre-commit hook 中运行 python3 .emap/emap_gate.py,以防未经验证的表单被合并。

继续之前请检查: 确认磁盘上存在这 3 个文件和 .emap/build-state.json(如果添加了设置项,也要确认该设置项存在)。不要假设写入已成功。然后继续执行第 2 步,或开始生成代码。

如果此环境无法运行 Python 3(例如基于浏览器的应用构建器),请告知开发者此处无法运行门禁,并说明他们必须在装有 Python 3 的机器上运行 python3 .emap/emap_gate.py,然后才能接受构建结果。不要默默跳过第 1 步。


第 2 步:合作伙伴归因(可选)

合作伙伴归因会将商家注册与合作伙伴在 EMAP 中的账户关联起来。 这是可选项——不设置也能注册,但合作伙伴将无法获得相应业绩归属。

所有模板都使用同一个常量 EMAP_PARTNER_KEY,并在其 <script> 代码块中设置。它是归因值,而不是机密:无论你构建哪种集成,它都会显示在页面的 JavaScript 中。

  • **集成 1 和 3(API):**模板会在第 1 步的 POST 请求 JSON 正文中将其作为 partner_key 发送。
  • **集成 2(重定向):**模板会将其作为 EMAP 的 secretKey URL 参数发送。尽管参数名称中有「secret」,但该值仍是同一个合作伙伴密钥,而且也会显示在重定向 URL 中。

即使部署后该密钥会显示在客户端,也绝不要将真实密钥值提交到公开代码仓库的源代码管理中——应将「在已部署的页面中」和「在 git 历史记录中」视为不同的暴露途径。

如果开发者还没有合作伙伴密钥,请引导他们查看开始之前 — 必填问题中的说明。 没有密钥也能进行集成;只是注册不会关联到合作伙伴。


第 3 步:选择集成模式

集成 1 — 完整表单集成 2 — 重定向交接集成 3 — 基于电子邮件的注册
工作方式合作伙伴承载全部 6 个步骤;浏览器将每个步骤直接 POST 到 EMAP 的 API合作伙伴承载单步表单(字段与集成 1 第 1 步相同);提交时,所有字段都会作为 URL 参数附加,浏览器随后重定向到 EMAP /signup?params合作伙伴承载单步表单;浏览器将第 1 步的字段直接 POST 到 EMAP 的 REST API;EMAP 会通过电子邮件向商家发送安全链接,以便完成申请
商家在哪里继续操作合作伙伴的网站 — 全部 6 个步骤EMAP,从第 2 步开始(重定向后立即继续)EMAP,从第 2 步开始(点击电子邮件链接后继续)
是否需要服务端代码?否。浏览器直接调用 EMAP否。重定向是浏览器导航否。浏览器直接调用 EMAP
在 EMAP 上自动提交?不适用 — 商家不会访问 EMAP当 first_name、last_name、company_name、phone 和 email 均已提供,且 EMAP 页面接受预填数据时会自动提交;否则商家需提交 EMAP 上已预填的表单不适用 — 浏览器已提交第 1 步
适用于需要在全部 6 个步骤中全面控制品牌展示、企业级集成简单嵌入、静态网站、最快速的集成提供简洁的合作伙伴侧第 1 步体验;商家点击电子邮件链接后,在 EMAP 上完成其余步骤

开发者已在问题 1 中选择了模式。不要再次询问;仅在回答他们有关模式差异的问题时使用此表。


构建集成 1:完整表单

**编写下方任何文件之前:**请检查此项目中是否存在 .emap/emap_gate.py。如果不存在,即使这里已经有 merchant-signup/ 或其他构建输出,也请先执行第 1 步。

继续之前,请先阅读 references/mode-1-fullform.md。

**必须严格依据 signup-steps-schema.json 构建。**编写或移植任何字段之前,请先在 signup-steps-schema.json 中查找该字段,并严格遵循文件中的定义——小组件的 type、API 字段名称/路由、每条验证规则(包括按国家/地区区分的 countryVariants),以及所有依赖/条件字段(dependsOn/visibleIf)。不要根据字段名称、Markdown 表格或说明文字推断这些内容——表单必须依据 JSON 文件构建,该文件是唯一可信来源。

步骤
  1. 使用 templates/integration-1/ 中的模板:

    • plain-html.html:包含完整的 6 步表单,无需构建步骤。这是标准参考版本。 可直接使用,或将其嵌入项目页面(参见 Step 0)。

    • SignupForm.tsx:相同表单的 React 组件,适用于任何 React 框架。

    • 对于其他组件框架,请挂载参考版本,或 逐项移植 plain-html.html, 不要仅根据文字说明重新生成表单。移植时需保留:

      • 相同的字段列表
      • 每个字段相同的控件类型
      • 相同的验证规则
      • 相同的条件逻辑
      • 相同的浏览器端 fetch() EMAP 调用

      使用 signup-steps-schema.json 作为逐字段规范,并遵循 references/stack-guide.md 中的移植规则。

    • 无论使用何种技术栈,EMAP 调用都必须保留在浏览器代码中。切勿添加调用 EMAP 的服务端路由、操作或控制器。

  2. 在文件 <script> 块的顶部设置这两个常量(搜索 "EDIT THESE"; 在 SignupForm.tsx 中,它们位于 FORM_LOGIC 字符串的顶部):

    var EMAP_BASE_URL = 'https://emap.epd.dev'; // EMAP's TEST server
    var EMAP_PARTNER_KEY = ''; // optional — see Step 2 above
    

    无需配置环境变量、.env 文件或服务端进程——这是唯一的配置步骤。https://emap.epd.dev 是 EMAP 的测试服务:请使用它进行构建和测试,并在上线前切换到 Easy Pay Direct 提供的生产 URL。

  3. 下拉列表直接从 EMAP 获取数据(GET {EMAP_BASE_URL}/api/partner/countries 等), 由模板中已有的 fetchDropdownData() 函数完成——无需实现代理路由。该函数已内置 EMAP_DROPDOWN_FALLBACKS 常量(来源于 references/dropdown-fallbacks.json),当实时请求出错、超时,或返回的 data 数组为空或缺失时会使用该常量兜底,因此下拉列表不会悄无声息地显示为零个选项。如果移植到其他技术栈,请一并移植 fetchDropdownData() 和内嵌的兜底数据。

  4. 了解步骤流程:

    • 每个步骤的请求 JSON 正文都包含 step_count(1–6),包括 Step 1 和 Step 4。
    • Step 1(POST /api/v1/signup,step_count: 1)返回 uuid 和 signup_token。收集:名字、姓氏、电子邮箱、电话、公司名称、网站、国家、年销售额、行业类型,以及(如果在美国)企业所在州。将 uuid 存入 localStorage('emap_uuid'),将令牌存入 localStorage('emap_signup_token')。
    • **Step 1 自动保存:**填写名字、姓氏、电子邮箱和电话后,离开其中任一字段时,会在后台将这些信息 POST 到 /api/v1/signup/auto-save(EMAP 会创建用户和 HubSpot 联系人,就像使用其自己的注册页面一样)。不会显示任何错误;Step 1 提交时会等待正在运行的自动保存完成。参见 references/mode-1-fullform.md。
    • Steps 2、3、5、6 会调用 POST /api/v1/application/step,并附上 uuid、最新的 signup_token 和相应的 step_count。
    • Step 4 会调用 POST /api/v1/ownership,并附上 uuid、最新的 signup_token、step_count: 4 和点号表示法的所有者字段。
    • **每个成功完成的步骤都会返回一个新的 signup_token;请用它覆盖旧令牌。**令牌缺失、错误或已过期(自上次保存步骤起 24 小时)时,EMAP 会对 Steps 2–6 返回 HTTP 403。页面加载时,不要恢复令牌已过期的已保存进度。参见 references/mode-1-fullform.md。
    • Step 6 成功后 → 清空 localStorage,并将顶层窗口重定向至 EMAP 的 {EMAP_BASE_URL}/upload-document/{uuid}?redirect=1 页面,商家可在该页面上传文件。
  5. 使用步骤 1 的数据预填步骤 2: 步骤 1 成功并且表单进入步骤 2 后,自动填入:

    • legal_name ← 步骤 1 中 company_name 的值
    • name(DBA /“doing business as”)← 步骤 1 中 company_name 的值 如果法定名称与经营名称不同,商户可以在步骤 2 中编辑这些字段。 仅在字段当前为空时预填(如果商户已输入内容或会话是从 localStorage 恢复的,则不要覆盖)。 此外,与 EMAP 自己的表单一样,当“Are you the primary contact?”为 Yes 时,使用步骤 1 中的 phone 预填所有者 1 的手机号码(phone.1,步骤 4)。切换为 No 时,如果该字段仍为步骤 1 中的号码,则将其清空;切换回 Yes 时恢复该号码。

    电话号码字段(phone、customer_service_telephone_number、phone.1、phone.2)使用 EMAP 自己的注册页面所用版本 intl-tel-input 22.0.2:国家/地区选择器(US 排在首位, 其次是 CA)、随输入格式化,例如 (202) 555-1234,并以示例号码作为占位符。只有 isValidNumber() 返回 true 时才允许提交,并发送 getNumber() 返回的 E.164 格式(+12025551234)。使用带有独立 SRI 校验的 <script> 标签加载 utils.js,并通过 window.intlTelInput.utils = window.intlTelInputUtils 将其传入。该库的 utilsScript 选项会注入不带完整性哈希的脚本。

  6. 步骤之间的返回导航: 进度条中的步骤编号可点击,前提是当前页面会话中已到达相应步骤(重新加载后则不可点击,因为之前的回答已丢失)。对每个 :disabled 输入框、选择框、 文本区域、单选框和复选框设置明显的样式(灰色背景、弱化的文字、not-allowed 光标),以便清楚表明字段为只读。 在步骤 2–6 显示“Back”按钮。返回时保留商户已输入的内容,商户可以编辑并重新提交该步骤:API 接受使用相同 uuid 和最新 signup_token 再次提交步骤。

    • 步骤 1 返回后为只读。POST /api/v1/signup 不能重放(该电子邮件现在已注册),因此应禁用其中的输入框,并让 Continue 直接前往步骤 2,而不调用 API。
    • 步骤 4:保存后,所有者身份信息为只读,与 EMAP 的单页注册流程一致。禁用“Are you the business owner?”、每位所有者的名字、姓氏和电子邮件,以及每位所有者的所有权百分比;步骤 4 中的其他内容(职务、电话、出生日期、SSN、地址、ID)仍可编辑。禁用的输入框不会被包含在 FormData 中,因此构建重新提交的载荷时,需按 ID 读取锁定字段的值。切勿将所有禁用控件的值都复制回来:每个州字段的文本框 / 美国州下拉框配对中,隐藏的一侧也处于禁用状态,且具有相同的 name,因此复制它会用空值覆盖选定的州(EMAP 会回答“State is required”)。步骤 2、3、5 和 6 没有锁定字段。
    • 每次重新提交都要发送该步骤自身的 step_count。绝不能跳过重新提交来直接跳到后续步骤。
    • 页面重新加载后,之前的回答会丢失,因此恢复的步骤上应隐藏“Back”按钮。
    • 上传文档页面不显示“Back”按钮:步骤 6 成功后会重定向到 EMAP。
  7. 处理条件字段:

    对每个条件字段,分别读取 visibleIf(显示条件)和 dependsOn(必填条件); 它们是不同的表达式。如果 EMAP 的 API 与其注册页面不一致,请以 API 为准。以下规则已遵循此原则;references/api-quirks.md 中 列出了每条规则的依据。

    • 在 第 1 步收集 industry_type(而非第 2 步),并通过 POST /api/v1/signup 提交。当 industry_type = other 时显示 industry_type_other,匹配时不区分大小写——实时 /api/partner/industry-types 端点的兜底 slug 是 Other(首字母大写),而不是 other(第 1 步)。
    • country=US → 显示 business_state(第 1 步)和 state.1(第 4 步)。
    • emap_country(第 1 步)= CA 或 business_organized = Sole-Proprietorship → 隐藏/禁用 federal_tax_id,并取消其必填要求(第 2 步)。否则,该字段会显示、必填,并根据 countryVariants 进行遮罩(见第 9 点)。任一字段变化时都要重新评估。
    • emap_country(第 1 步)= US → 隐藏 business_register_number 并取消其必填要求(第 2 步)。这是 API 唯一授予的豁免——不要将其扩展到 Puerto Rico 或 CA + Sole-Proprietorship。
    • is_physical_address_same_as_legal_address=0 → 显示实际地址区块(第 2 步)。
    • marketingModel 包含 2 → 显示 subscription_frequency;如果 frequency=3,则显示 subscription_frequency_other(第 2 步)。
    • fulfillment_by 为 Vendor 或 Others → 显示 fullfillment_company(两个 l,第 3 步)。
    • primary_contact=0 → 显示 first_name.1、last_name.1、email.1、primary_contact_job_title(第 4 步)。
    • ownership_percentage.1 < 51 → 显示 Owner 2 区块(第 4 步)。
    • country.1=US → 显示 driver_license_state.1 和 driver_license_expiration_date.1(第 4 步)。
    • emap_country(第 1 步)= CA → 显示 institution_number + customer_pay_currency(第 5 步)。
    • bad_experience=true → 显示 bad_experience_happened(第 6 步)。
    • howdidyouhear 为 "Other"、"Friend" 或 "Live Event / Trade Show" → 显示 hear_about_us_other (第 6 步)。按 slug 或名称文本匹配(Other、Friend、Live-Event-/-Trade-Show);该 端点不返回 id。
  8. 控件类型——绝不要猜测,始终根据 schema 确定。 这些字段使用硬编码值集,而非动态下拉数据;而且过去多次被错误地生成为包含原始值的 <select>。渲染前,请在 signup-steps-schema.json 中检查每个字段的 type:

    • marketingModel → 复选框组(staticDropdowns.marketing_model),而非下拉框。渲染 label 文本(例如 "Recurring/Continuity/Subscription"),提交整数 value。
    • is_physical_address_same_as_legal_address、primary_contact、bankruptcy_filed.1/.2、 bankruptcy_discharged.1/.2、current_processing、bad_experience、 multiple_merchant_accounts、leave_deposit → 单选按钮(Yes/No),而非下拉框。
    • terms_and_conditions_agreed → 复选框,而非下拉框。
    • 由 optionsSource: dynamicDropdownEndpoints.*(country、industry_type、 shopping_cart、howdidyouhear 等)提供选项的字段,正确的控件类型是 <select>。
  9. 按国家进行字段验证——必须强制执行,而不只是显示提示。 在 signup-steps-schema.json 中读取这些字段的 countryVariants,并将相应的 maxLength/minLength/pattern 同时配置到输入框的 HTML 属性和提交时的 JS 检查中(与下方强制执行 SSN 掩码的方式相同)——标签或占位符写着“9 位数字”并不会自动起到任何作用:

    • routing_number — US:必须正好是 9 位数字(^[0-9]{9}$)。
    • account_number — US:8–17 个字符。
    • federal_tax_id — 仅限数字;US/CA/PR 使用 XXX-XX-XXXX(3-2-4 位分组)掩码,其他所有国家使用 XX-XXXXXXX(2-7 位分组)掩码。这与该字段通用的 ^[0-9A-Za-z\-]+$ 模式和 “EIN” 占位符可能给人的暗示相反——无论哪个国家,该字段都绝不能接受自由格式的字母数字内容,即便非 US/CA/PR 国家/地区的标签显示为“...或等效的 Corporation Tax Number”。请参阅 signup-steps-schema.json 中该字段的 countryVariants。
    • institution_number — 请参阅架构中的 countryVariants/pattern。
  10. 卡片支付百分比(第 3 步): card_swiped + customer_entered + staff_entered 必须等于 100。 在客户端进行验证,不满足时阻止提交。

  11. SSN/SIN(第 4 步): 对于 US/CA/PR,应用 Cleave.js 掩码 blocks:[3,2,4] delimiters:["-","-"] 将格式设为 XXX-XX-XXXX,标签为“SSN/SIN”。验证:ssn.replace(/-/g,'').length >= 9。 对于其他所有国家/地区,显示普通的非掩码文本字段,标签为“SSN(或等效的个人 Tax ID)”, 除必填外不进行格式检查。

    决定此规则的国家/地区是 country_from_step1,即第 1 步选择的单一公司成立国家/地区; ssn.1 和 ssn.2 均使用该值。它不是每位所有者自己的 country.1/country.2; 这两个值仅用于控制驾驶执照字段。

    • 所有者 1 的 SSN(ssn.1)始终必填。只有在显示所有者 2 区块时,所有者 2 的 SSN(ssn.2)才必填(ownership_percentage.1 < 51)。
  12. 出生日期(第 4 步): 所有者年龄必须在 18 至 100 岁之间。 maxDate = today − 18 years,minDate = today − 100 years。 在客户端强制执行此规则(日期选择器的 min/max 属性加上提交时的检查)。EMAP 的 API 不会检查此项,因此表单检查是唯一的检查;请参阅 references/api-quirks.md。

    • 所有日期字段均显示 YYYY-MM-DD(business_formed、dob.*、 driver_license_expiration_date.*、bankruptcy_discharged_date.*)。不要直接渲染 <input type="date">:浏览器会按访客所在地区的格式显示日期(例如 US 的 MM/DD/YYYY)。模板使用带 Cleave.js 掩码的文本框(date: true, datePattern: ['Y','m','d'], delimiter: '-'),并在日历图标上方覆盖一个透明且无名称的原生日期输入框;选取的日期会复制到文本框中。每个步骤提交时,都会拒绝非有效 YYYY-MM-DD 日期的手动输入值。
  13. 处理每个步骤中的所有响应形式(请参阅 references/api-errors.md):

    • {"status":true} → 进入下一步。
    • 步骤 2–6 收到 HTTP 403 → 清除已保存的进度;告知商户从 EMAP 发送的邮件继续,或开始新的申请。
    • HTTP 422 → 显示各字段的错误。对于表单中不显示的字段(例如 partner_key)的错误,应显示在页面级提示框中。
    • HTTP 429 → 请商户稍候重试。
    • HTTP 5xx → 显示通用提示“请重试”。
  14. 运行 verify_form.py,不要手动检查是否符合架构。 不要对照架构手动抽查各个字段(小组件类型、硬编码标签、国家验证模式、条件显示/隐藏逻辑、下拉菜单路由/回退)——请改为运行 verify_form.py(参见 验证:架构符合性),并准确修复它报告的问题。该检查具有确定性,几乎瞬间即可完成;手动重复检查只是在同一件事上白费功夫。 这不能替代在线测试。 脚本只能发现架构与代码不匹配,无法发现两者对 API 的理解是否都错了(参见 verify/README.md 中的“此脚本无法发现的问题”)。检查通过后,针对一个有代表性的边缘情况(例如,非 CA 地区的独资企业),使用你能控制的电子邮件地址,在 EMAP 的测试服务上提交一个 6 步注册。切勿在 EMAP 生产服务上创建测试申请。


构建集成 2:重定向交接

**在编写以下任何文件之前:**检查此项目中是否存在 .emap/emap_gate.py。如果不存在——即使此处已存在 merchant-signup/ 或其他构建输出——也请先执行步骤 1。

继续之前,请先阅读 references/mode-2-redirect.md。

严格根据 signup-steps-schema.json 构建。 编写任何字段之前,都要在 signup-steps-schema.json 中查找该字段并严格遵循其定义——包括小组件 type、 API 字段名称、每条验证规则(包括按国家区分的 countryVariants),以及所有依赖字段/条件字段(dependsOn/visibleIf)。不要根据字段名称、Markdown 表格或说明文字推断任何内容——JSON 文件是构建表单时必须遵循的唯一事实来源。

集成 2 的一个例外: 对于 country 和 industry_type,请发送架构中该字段 integration2UrlValue 指定的值(显示名称),而不是代码或 slug。EMAP 的 /signup 页面会按名称匹配这些值。请将代码/slug 保存在 data-code/data-slug 属性中,供表单自身的条件逻辑使用。

概述

集成 2 是一个单步表单——它收集的字段与集成 1 第 1 步完全相同(商户基本信息)。不包含“条款与条件”复选框。提交时,所有表单字段都会序列化为 URL 查询参数,浏览器随后会重定向到 {EMAP_BASE_URL}/signup?{params}。EMAP 会读取这些参数并预填表单。当 first_name、last_name、company_name、phone 和 email 均已填写时,EMAP 页面会验证预填数据;如果验证通过,就会代商户提交,商户随后进入第 2 步。否则,商户需要检查 EMAP 预填的表单并提交。(在 EMAP 测试服务上观察到;参见 references/mode-2-redirect.md。)

收集的字段(与集成 1 第 1 步相同)
字段类型备注
first_name文本必填
last_name文本必填
email电子邮件必填
phone电话必填
company_name文本必填
websiteURL必填
country选择必填 — 传入完整国家名称(例如 United States),不要传 2 个字符的代码
business_state选择仅当 country = United States 时必填
annual_sales数字必填
industry_type选择必填 — 从 EMAP API(/api/partner/industry-types)加载,将 name 用作值
industry_type_other文本条件必填 — 当 industry_type = Other 时显示
promo_code文本可选

没有条款与条件复选框。 不要添加 — 商户会在第 6 步之后于 EMAP 上同意条款。

步骤
  1. 使用 templates/integration-2/ 中的模板:

    • plain-html.html:一个客户端表单,用于构建并跳转到重定向 URL。这是规范参考。可直接使用,或将其嵌入(参见 第 0 步)。
    • SignupForm.tsx:相同表单的 React 组件,适用于任何 React 框架。
    • 对于其他组件框架,可挂载参考实现或按 1:1 移植(参见 references/stack-guide.md)。重定向必须是浏览器 导航,绝不能是基于提交数据构建的服务端重定向。
    • 第 1 步自动保存: 填写名字、姓氏、电子邮件和电话后,离开其中任一字段时,都会在后台将这些信息 POST 到 /api/v1/signup/auto-save,与集成 1 和 3 的做法相同。EMAP 自己的 /signup 页面会跳过从重定向进入的访客的自动保存,因此这是保存填写了个人信息但从未提交的商户信息的方式。不会显示任何错误,提交时会等待正在运行的自动保存完成,然后再跳转。参见 references/mode-2-redirect.md。
  2. 提交时 — 构建重定向 URL:

    const params = new URLSearchParams();
    params.set('first_name', formData.get('first_name'));
    params.set('last_name',  formData.get('last_name'));
    params.set('email',      formData.get('email'));
    params.set('phone',      formData.get('phone'));
    params.set('company_name', formData.get('company_name'));
    params.set('website',    formData.get('website'));
    params.set('country',    formData.get('country'));       // full name
    if (formData.get('business_state')) params.set('business_state', formData.get('business_state'));
    params.set('annual_sales', formData.get('annual_sales'));
    params.set('industry_type', formData.get('industry_type'));
    if (formData.get('promo_code'))    params.set('promo_code', formData.get('promo_code'));
    // UTM pass-through (see step 3)
    // navigateTop() navigates the top-level window (falls back to this frame), so the
    // handoff also works when the form is embedded in an iframe.
    navigateTop(EMAP_BASE_URL + '/signup?' + params.toString());
    
  3. 传递 UTM 参数。 从当前页面 URL 读取 utm_campaign、utm_source、utm_medium、utm_term、 utm_content 和点击 ID gclid、gbraid、wbraid、fbclid,并将它们附加到重定向 URL。模板会自动完成此操作。

    集成 1 和 3 改为调用 API,因此其模板会将相同的值以及 fbclid 作为第 1 步 POST /api/v1/signup 请求正文中的额外字段发送。getTracking() 会从 落地页 URL 读取这些值,并将其用作当前归因信息,同时将它们存储在 localStorage(emap_tracking)中, 保留 30 天。URL 上的新参数会替换已存储的参数(旧参数会被丢弃);如果 URL 上没有参数,则会复用已存储的参数,因此刷新页面或之后直接返回仍会保留来源。仅在有值时发送;EMAP 会忽略缺失的参数。

    仅在第 1 步保存。 EMAP 会将第 1 步请求中的归因信息以扁平键值的形式存储在 utm_tracking 中(存储于用户和申请记录)。第 2 至第 6 步不会发送这些信息,且 EMAP 会忽略这些端点上的此类字段,因此商户在注册过程中更改 URL 不会改变已保存的信息。HubSpot 交易和联系人属性(utm_*、gclid、gbraid、wbraid、 fbclid)会保留第 1 步的值。

  4. 设置文件 <script> 块顶部的两个常量(搜索 "EDIT THESE"):

    const EMAP_BASE_URL = 'https://emap.epd.dev'; // EMAP's TEST server
    const EMAP_PARTNER_KEY = ''; // optional — see Step 2 above
    

    无需配置环境变量、.env 文件或服务端进程。上线前,将 EMAP_BASE_URL 切换为 Easy Pay Direct 提供的生产 URL。

  5. 了解个人身份信息(PII)的去向。 重定向 URL 会在查询字符串中携带商家名称、电子邮箱和电话号码。你页面上的 Referrer-Policy 无法保护该 URL:加载该 URL 的是 EMAP 页面,因此只有 EMAP 自身的 Referrer-Policy 和处理方式才能防止该 URL 出现在其页面发送的 Referer 标头中。请告知开发者,这是重定向模式本身的特性。如果无法接受,建议使用集成 3,该集成会通过 POST 请求正文发送数据。

  6. 使用 EMAP 测试服务进行测试:

    • 缺少自动提交字段:将 phone 留空。EMAP 会显示预填好的表单,由商家提交。
    • 完整预填:为每个字段填写有效数据。EMAP 会代商家提交,商家随后会进入第 2 步。如果 EMAP 页面拒绝某个值(例如无法识别的网站),商家会看到 EMAP 预填好的表单。请参阅 references/mode-2-redirect.md。
    • 运行关卡检查 python3 .emap/emap_gate.py(请参阅 验证:符合架构规范),然后在告知开发者表单已完成之前, 验证:安全性与覆盖范围。以上手动测试不能替代这两项检查。只有关卡检查输出 PASS,构建才算完成。

构建集成 3:基于电子邮件的注册

在编写下方任何文件之前: 检查此项目中是否存在 .emap/emap_gate.py。如果不存在——即使 merchant-signup/ 或其他构建输出已存在于此处——也要先执行步骤 1。

继续之前,请阅读 references/mode-3-api.md。

严格依据 signup-steps-schema.json 构建。 编写任何字段之前,都要先在 signup-steps-schema.json 中查找,并严格遵照其中的定义——控件 type、 API 字段名称、每条验证规则(包括按国家/地区区分的 countryVariants),以及所有 依赖字段或条件字段(dependsOn/visibleIf)。不得根据字段名称、Markdown 表格或说明文字推断 这些内容——JSON 文件是构建表单必须遵循的唯一事实来源。

步骤
  1. 使用 templates/integration-3/ 中的模板:

    • plain-html.html:无需构建步骤的客户端表单,是规范参考。 可直接使用,也可嵌入(参见 步骤 0)。
    • SignupForm.tsx:相同表单的 React 组件,适用于任何 React 框架。
    • 对于其他组件框架,可挂载参考实现,或按 1:1 的方式移植(参见 references/stack-guide.md)。 表单会直接从浏览器向 {EMAP_BASE_URL}/api/v1/signup 发送 POST 请求,并在 JSON 请求体中包含 partner_key(若已设置)和 trigger_email: true——此标记会告知 EMAP 在同一次调用中发送欢迎/验证邮件;若不包含此标记,系统会创建账户,但不会发送邮件。注意:公司名称的 API 字段是 name,而不是 company_name——模板已在提交时完成映射(见其中的注释);这是一个仅用于 UI 的字段名称,旨在让标签更清晰。在此之前,填写姓名、电子邮件和电话后,表单会在后台自动将这些信息保存到 /api/v1/signup/auto-save,与集成 1 的步骤 1 完全一致(参见 references/mode-3-api.md)。
  2. 设置文件顶部 <script> 块中的两个常量(搜索 "EDIT THESE"):

    const EMAP_BASE_URL = 'https://emap.epd.dev'; // EMAP's TEST server
    const EMAP_PARTNER_KEY = ''; // optional — see Step 2 above
    

    无需配置环境变量、.env 文件或服务器进程。上线前,将 EMAP_BASE_URL 切换为 Easy Pay Direct 提供的生产 URL。

  3. 在客户端处理所有响应形式(完整表格参见 references/api-errors.md)。表单供商户使用,因此每条消息都应面向商户:

    • {"status":true,"uuid":"..."} → 显示“查看电子邮件”,并注明链接发送到的地址。 切勿显示、记录或存储 uuid:任何持有它的人都可以继续该申请。
    • {"verificationLink":true,"url":"..."}(现有用户)→ 显示相同的“查看电子邮件”面板,并提示该电子邮件已注册。不要跳转到 url。
    • {"status":false,"message":"Company already exists"} → 告知商户已有申请,并请其查看收件箱或联系 Easy Pay Direct。
    • HTTP 422 → 显示 response.errors 中每个字段的错误。
    • HTTP 429 → 告知商户等待后重试。
  4. 使用 EMAP 测试服务进行测试,提交一个由你控制的唯一电子邮件地址。 确认“查看电子邮件”提示已显示,且邮件已送达。切勿在生产环境中测试。


验证:架构符合性

在项目根目录运行门禁。它会针对已跟踪的表单运行 verify_form.py,这是一个不涉及 LLM 阅读的确定性脚本:

python3 .emap/emap_gate.py

不要改为手动抽查字段,并与 signup-steps-schema.json 对照。无论你使用什么 Agent,只有门禁打印 PASS,构建才算完成。 verify/README.md 介绍了整个流程(修复 → 定向重跑 → 再次运行门禁,最多运行 5 次且仍未通过),说明了 5 类检查各自涵盖的内容,以及脚本在结构上无法捕获的问题:有些 bug 需要在 EMAP 测试服务上进行实时测试,而不能只做架构与代码的差异比对。


验证:安全性与覆盖情况

上线前,请按 references/security-checklist.md 逐项检查。

快速自检——上文的 验证:架构符合性 仅检查架构符合性;不会检查部署/配置接线,因此以下项目需要人工检查,不能只凭 verify_form.py 运行无误就算通过:

  • 你的网站使用 HTTPS。
  • EMAP_BASE_URL 指向 Easy Pay Direct 提供的生产环境 URL,而不是测试服务器 https://emap.epd.dev。
  • 在已部署的文件中直接设置 EMAP_PARTNER_KEY,不要保留占位符 '',除非你有意跳过归属追踪。请与开发者确认他们接受该密钥在客户端可见(见上文第 2 步)——不要擅自替他们决定。
  • 集成 3:成功面板不会显示申请的 uuid,且除了集成 1 自己的 localStorage 恢复键外,没有任何表单会在其他位置存储它。
  • 首次点击后禁用提交按钮(防止重复提交)。
  • 面向用户显示通用错误信息——不要将 EMAP 的原始错误消息原样暴露给商户。
  • 嵌入的下拉菜单回退数据确实包含在已部署的文件中,没有在构建步骤中被移除——请确认 EMAP_DROPDOWN_FALLBACKS(或模板中的等效常量)仍存在于实际部署的内容中,并且回退路径确实可用(例如,短暂将 EMAP_BASE_URL 指向无法访问的主机进行测试),而不只是定义了却从未调用。
  • 没有服务器端代码调用 EMAP。 请检查路由处理程序、API 路由、Server Action、加载器和 action、SSR 数据获取、控制器、curl/wp_remote_post 以及无服务器函数(references/stack-guide.md 按框架列出了这些内容)。如果存在此类代码,说明集成构建错误。每个 EMAP 请求都必须来自商户的浏览器,这样 EMAP 才能基于 IP 识别商户的 IP 并实施速率限制。

防护措施

任何技术栈都可以,但按设计,所有 EMAP 调用都由商户的浏览器发起。 切勿以任何形式通过合作伙伴的服务器转发 EMAP 请求:

  • API 路由、Server Action、加载器或 action
  • SSR 数据获取
  • 控制器、curl/wp_remote_post 或无服务器函数

EMAP 根据调用方的 IP 对注册请求实施速率限制,而服务器代理会让所有商户共用一个 IP。EMAP API 允许通过 CORS 从浏览器发起调用。因此,合作伙伴密钥常量会在已部署页面的源代码中可见。这是已接受且经开发者确认的权衡(见上文第 2 步),不是需要通过添加后端代理来“修复”的疏漏。 仍然绝不能将合作伙伴密钥输出或记录到任何第三方分析/错误工具中,也绝不能将真实密钥值提交到公开源代码仓库——“在已部署页面中可见”与“在 git 历史记录/日志聚合工具中可见”属于不同的暴露面,这里只接受前者。 绝不能自行在任何地方存储 PII(姓名、邮箱、电话)——EMAP 是记录系统,而且此表单没有自己的后端来存储这些信息。 绝不能添加文档上传字段——EMAP 会在第 6 步之后通过自己的 UI 收集文档。 不要修改字段验证规则——无论此表单允许提交什么内容,EMAP 都会拒绝不符合自身服务器端规则的提交。

评论

支持 Markdown