Skip to content

feat: V2 API support, new tools, and API docs#21

Merged
jack-arturo merged 3 commits intoverygoodplugins:mainfrom
GravityKit:feat/v2-api-and-additional-tools
Feb 18, 2026
Merged

feat: V2 API support, new tools, and API docs#21
jack-arturo merged 3 commits intoverygoodplugins:mainfrom
GravityKit:feat/v2-api-and-additional-tools

Conversation

@zackkatz
Copy link
Contributor

Summary

This PR upgrades the MCP EDD server to use V2 API endpoints for products and customers, adds three new read-only tools, improves schema resilience, and includes comprehensive API documentation.

Tested against live EDD stores (GravityKit, WP Fusion, Barn2) to validate V2 behavior across different configurations.

V2 API migration

  • Products — always use V2, gaining sku, richer category/tags (object arrays), and server-side search, category, and tag filtering. Normalizes both standard arrays and numeric-keyed object responses.
  • Customers — always use V2, gaining date_created, additional_emails, date preset filtering (today/yesterday), date range filtering (YYYYMMDD), and unified &customer= param for single lookups by ID or email.

V2 is a strict superset of V1 — no functionality is lost.

New tools

Tool Description
edd_get_discount_by_code Case-insensitive discount lookup by code string
edd_list_active_discounts List only currently active discount codes
edd_get_stats_by_preset Stats with expanded date presets (today, this_week, this_quarter, this_year, etc.)

Schema improvements

  • ProductInfoSchema: add sku; widen category/tags for V2 object arrays
  • ProductsResponseSchema: accept both array and numeric-keyed object
  • CustomerInfoSchema: id now optional (V2 returns customer_id); add additional_emails, date_created
  • File objects: add V2 index and attachment_id fields
  • exp_length: accept both number and string

Resilience fixes

  • EDD "No X found!" API responses treated as empty results, not thrown errors
  • V2 numeric-keyed product objects gracefully normalized via Object.values()
  • startDate/endDate pair validation in customer listing tool

Documentation

  • New docs/API.md — complete tool reference with response formats, date presets, and API version details

Test plan

  • npm run build compiles cleanly
  • npm test — 40 unit tests pass (was 26, added 14)
  • npm run lint — no errors
  • Verified against live GravityKit EDD store (V2 responses with numeric-keyed objects)
  • Verified V2 endpoints respond on WP Fusion and Barn2 stores
  • Confirmed standard EDD product fields via source code review (class-edd-api.php)

Upgrade products and customers endpoints to always use V2 API, which
returns richer data (sku, category/tag objects, date_created,
additional_emails) while remaining fully backward-compatible with V1
responses.

## V2 API migration

- Products: always use V2 with search, category, and tag filtering;
  normalize both array and numeric-keyed object responses
- Customers: always use V2 with date preset and date range filtering;
  use `&customer=` param for single lookups (accepts ID or email)
- Add `buildV2Url()` for V2 endpoint construction
- Remove `buildPublicUrl()` (V2 with auth is always used)

## New tools

- `edd_get_discount_by_code` — case-insensitive lookup by discount code
- `edd_list_active_discounts` — filter to only active discount codes
- `edd_get_stats_by_preset` — stats with date presets (today, this_week,
  this_quarter, this_year, etc.)

## Schema improvements

- `ProductInfoSchema`: add `sku`, widen `category`/`tags` to accept V2
  object arrays, widen `exp_length` to accept string
- `ProductSchema`: add `index` and `attachment_id` to file objects
- `ProductsResponseSchema`: accept both array and numeric-keyed object
- `CustomerInfoSchema`: make `id` optional (V2 returns `customer_id`),
  add `additional_emails` and `date_created`
- Add input schemas for all new tools with date pair validation

## Resilience

- Treat EDD "No X found!" API responses as empty results, not errors
- Gracefully handle V2 numeric-keyed product objects (Object.values)
- Validate startDate/endDate pairs in customer listing tool

## Documentation

- Add docs/API.md with complete tool reference, response formats,
  date presets, and API version details

## Tests

- 40 unit tests covering V2 URL construction, response normalization,
  date filtering, search/category/tag params, discount by code,
  active discount filtering, stats presets, and empty result handling

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

Warning

Rate limit exceeded

@zackkatz has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 56 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Migrates the client and tools to use the EDD V2 API, adds V2-aware methods for products, customers, discounts, and stats (including response normalization and client-side filters), introduces comprehensive API docs, updates types, and expands unit tests and tool signatures for new inputs.

Changes

Cohort / File(s) Summary
API Documentation
docs/API.md
Adds a new comprehensive API reference covering authentication, endpoints (Products, Customers, Sales, Discounts, Statistics, File Download Logs), request/response examples, error handling, pagination, and date formats.
V2 API Client Implementation
src/edd-client.ts
Replaced public URL helper with buildV2Url (auth-attached), migrated product and customer endpoints to V2, added normalizeStatsResponse, getStatsByPreset, getDiscountByCode, listActiveDiscounts, and updated listProducts/listCustomers to accept new filter/date options and normalize varied V2 responses.
Tool Integration & Expansion
src/index.ts
Expanded edd_list_products and edd_list_customers inputs/output (search, category, tag, date presets/range, sku, totalDownloads), added tools: edd_get_discount_by_code, edd_list_active_discounts, and edd_get_stats_by_preset, with input validation and updated output formats.
Type Definitions
src/types.ts
Added/relaxed V2-compatible schema fields: product category, tags, optional sku; exp_length accepts number
Test Suite Updates
tests/unit/edd-client.test.ts
Updated tests to assert authenticated /v2/ URLs and params; added tests for numeric-keyed product normalization, product filters (search/category/tag), customer date presets/ranges, discount lookup and active filtering, stats by preset/range, and V2-specific error/no-results handling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: V2 API support, new tools, and API docs' accurately summarizes the three main changes in the changeset: V2 API migration, new tools, and documentation.
Description check ✅ Passed The description comprehensively covers all major changes including V2 API migration, new tools, schema improvements, resilience fixes, and documentation, with testing details.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
src/edd-client.ts (4)

243-263: buildV2Url is defined after its first call site — consider reordering for readability.

buildV2Url (line 246) is called on line 226 but defined after getProduct. While method ordering within a class is not a runtime issue in TypeScript, placing private helpers before their first use improves readability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/edd-client.ts` around lines 243 - 263, The private helper buildV2Url is
defined after its first call in getProduct; move the buildV2Url method so it
appears before the first usage (i.e., place buildV2Url above getProduct or any
other method that calls it) to improve readability; ensure you preserve the
existing signature and behavior (private buildV2Url(endpoint: string, params:
Record<string, string|number|undefined> = {}): string) and update nothing else
in getProduct or callers.

525-536: getDiscountByCode fetches all discounts on every call — document or mitigate the cost.

Passing number: -1 fetches the entire discount list from the API on every invocation. For stores with many discounts, this could be slow and bandwidth-heavy. The JSDoc is clear about this, which is good. Consider caching discounts or at least noting this in the API docs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/edd-client.ts` around lines 525 - 536, getDiscountByCode currently calls
listDiscounts({ number: -1 }) on every invocation which forces fetching the
entire discount list; update the implementation and docs to avoid repeated
full-list fetches by either: 1) adding a simple in-memory cache inside the
EddClient class keyed by store and invalidated on discount mutations (use
getDiscountByCode to consult the cache first and fall back to listDiscounts then
populate cache), or 2) adding an optional parameter to getDiscountByCode like
useCache?: boolean or maxFetch?: number so callers can opt into cached lookups
or limit results; also update the JSDoc for getDiscountByCode and mention
listDiscounts usage and cache behavior so callers know the cost.

484-501: getStatsByPreset largely duplicates getStats — consider consolidating.

The response normalization logic in getStatsByPreset (lines 494-500) is identical to getStats (lines 374-382). You could have getStats accept an optional date parameter and reuse it, or extract the normalization into a shared helper.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/edd-client.ts` around lines 484 - 501, Consolidate duplicate
response-normalization between getStatsByPreset and getStats by either adding an
optional date parameter to getStats (e.g., change getStats(type:
'sales'|'earnings', date?: string)) and have getStats build the URL via
this.buildUrl('stats/', { type, date }) and call this.request, or extract the
normalization logic into a shared helper (e.g.,
normalizeStatsResponse(response): StatsResponse['stats']) and have both getStats
and getStatsByPreset call that helper; update getStatsByPreset to delegate to
the unified getStats or the helper so the duplicated block (the "if ('stats' in
response ... return { [type]: response[type] }") is removed and only one place
handles normalization.

182-185: Good resilience: "No X found!" treated as empty results.

The regex /^No \w+ found!?$/i correctly matches EDD empty-result messages like "No customers found!". However, the pattern is limited to single-word entity names (e.g., customers, products). If EDD returns error messages with multi-word entity names (e.g., "No download logs found!"), the regex would fail to match due to the space. Consider updating to /^No [\w\s]+ found!?$/i if broader compatibility is desired, though current test coverage only demonstrates single-word entities.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/edd-client.ts` around lines 182 - 185, The current check on data.error
uses a regex that only matches a single-word entity after "No" so multi-word
messages like "No download logs found!" slip through; update the regex used in
the data.error conditional to allow whitespace inside the entity name (i.e.,
allow one-or-more word-or-space characters after "No") so those messages are
treated as empty results, keep the existing throw new Error(...) behavior for
true errors, and update/add tests covering multi-word entity messages; refer to
the data.error check and the throw new Error(...) line in src/edd-client.ts when
making the change.
src/types.ts (1)

309-329: Import V2 input schemas in index.ts instead of duplicating them inline.

GetDiscountByCodeInputSchema, ListActiveDiscountsInputSchema, and GetStatsByPresetInputSchema are exported from src/types.ts but not referenced anywhere. Tools 14, 16, and 17 in src/index.ts define equivalent inline Zod schemas instead of importing these definitions. Import and use the schemas from types.ts to avoid duplication and maintain consistency with the coding guideline for Zod schema usage in src/**/*.ts files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types.ts` around lines 309 - 329, Replace the duplicated inline Zod
schemas used by the tool definitions in src/index.ts (the schemas defined for
tools 14, 16, and 17) with the exported schemas from src/types.ts: import
GetDiscountByCodeInputSchema, ListActiveDiscountsInputSchema, and
GetStatsByPresetInputSchema and use those in the corresponding tool input
definitions instead of recreating them inline so the tools reference the single
source-of-truth schema names (GetDiscountByCodeInputSchema,
ListActiveDiscountsInputSchema, GetStatsByPresetInputSchema).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/API.md`:
- Around line 549-551: The docs line claiming "retries failed requests up to 3
times" is misleading because edd-client.ts uses retries = 3 with the loop
condition "attempt <= retries", resulting in 3 total attempts (initial attempt +
2 retries); update the API docs wording to clarify this by changing the sentence
to something like "The server will attempt failed requests up to 3 attempts
total (initial attempt plus 2 retries) with exponential backoff (1s, 2s, 4s)" so
it matches the behavior of the retries variable and the attempt <= retries
check.

In `@src/index.ts`:
- Around line 573-574: The tool comment numbering is inconsistent (skipping
"Tool 15"); locate the block comments labeled "Tool 14", "Tool 16: List Active
Discounts", and "Tool 17" in src/index.ts and renumber them sequentially (e.g.,
change "Tool 16" -> "Tool 15" and "Tool 17" -> "Tool 16" or adjust all
subsequent tool comment labels) so the comment headings match the actual order
of tools and avoid the missing "Tool 15" gap.

---

Nitpick comments:
In `@src/edd-client.ts`:
- Around line 243-263: The private helper buildV2Url is defined after its first
call in getProduct; move the buildV2Url method so it appears before the first
usage (i.e., place buildV2Url above getProduct or any other method that calls
it) to improve readability; ensure you preserve the existing signature and
behavior (private buildV2Url(endpoint: string, params: Record<string,
string|number|undefined> = {}): string) and update nothing else in getProduct or
callers.
- Around line 525-536: getDiscountByCode currently calls listDiscounts({ number:
-1 }) on every invocation which forces fetching the entire discount list; update
the implementation and docs to avoid repeated full-list fetches by either: 1)
adding a simple in-memory cache inside the EddClient class keyed by store and
invalidated on discount mutations (use getDiscountByCode to consult the cache
first and fall back to listDiscounts then populate cache), or 2) adding an
optional parameter to getDiscountByCode like useCache?: boolean or maxFetch?:
number so callers can opt into cached lookups or limit results; also update the
JSDoc for getDiscountByCode and mention listDiscounts usage and cache behavior
so callers know the cost.
- Around line 484-501: Consolidate duplicate response-normalization between
getStatsByPreset and getStats by either adding an optional date parameter to
getStats (e.g., change getStats(type: 'sales'|'earnings', date?: string)) and
have getStats build the URL via this.buildUrl('stats/', { type, date }) and call
this.request, or extract the normalization logic into a shared helper (e.g.,
normalizeStatsResponse(response): StatsResponse['stats']) and have both getStats
and getStatsByPreset call that helper; update getStatsByPreset to delegate to
the unified getStats or the helper so the duplicated block (the "if ('stats' in
response ... return { [type]: response[type] }") is removed and only one place
handles normalization.
- Around line 182-185: The current check on data.error uses a regex that only
matches a single-word entity after "No" so multi-word messages like "No download
logs found!" slip through; update the regex used in the data.error conditional
to allow whitespace inside the entity name (i.e., allow one-or-more
word-or-space characters after "No") so those messages are treated as empty
results, keep the existing throw new Error(...) behavior for true errors, and
update/add tests covering multi-word entity messages; refer to the data.error
check and the throw new Error(...) line in src/edd-client.ts when making the
change.

In `@src/types.ts`:
- Around line 309-329: Replace the duplicated inline Zod schemas used by the
tool definitions in src/index.ts (the schemas defined for tools 14, 16, and 17)
with the exported schemas from src/types.ts: import
GetDiscountByCodeInputSchema, ListActiveDiscountsInputSchema, and
GetStatsByPresetInputSchema and use those in the corresponding tool input
definitions instead of recreating them inline so the tools reference the single
source-of-truth schema names (GetDiscountByCodeInputSchema,
ListActiveDiscountsInputSchema, GetStatsByPresetInputSchema).

- Fix retry docs: clarify "3 total attempts" not "retries 3 times"
- Fix tool numbering: 14 → 15 → 16 (was skipping 15)
- Move buildV2Url above first call site for readability
- Consolidate getStatsByPreset into getStats(type, date?) with shared
  normalizeStatsResponse helper to eliminate duplication
- Widen "No X found!" regex to handle multi-word entities like
  "No download logs found!" via [\w\s]+
- Add test for multi-word empty-result messages (41 tests total)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/edd-client.ts (1)

205-222: buildV2Url duplicates the auth and param-iteration logic from buildUrl

Auth token assignment and the Object.entries loop are identical in both methods. This is an optional DRY cleanup—either compute the v2 base URL and delegate to buildUrl, or accept a version argument.

♻️ Suggested refactor
-  private buildV2Url(endpoint: string, params: Record<string, string | number | undefined> = {}): string {
-    const base = this.apiUrl.replace(/\/$/, '');
-    const v2Base = `${base}/v2/`;
-    const url = new URL(endpoint, v2Base);
-
-    url.searchParams.set('key', this.apiKey);
-    url.searchParams.set('token', this.apiToken);
-
-    for (const [key, value] of Object.entries(params)) {
-      if (value !== undefined) {
-        url.searchParams.set(key, String(value));
-      }
-    }
-
-    return url.toString();
-  }
+  private buildV2Url(endpoint: string, params: Record<string, string | number | undefined> = {}): string {
+    const base = this.apiUrl.replace(/\/$/, '');
+    const v2Base = `${base}/v2/`;
+    // Temporarily override apiUrl so buildUrl resolves against the V2 base
+    const saved = this.apiUrl;
+    // Alternatively, inline and reuse buildUrl with a modified base via a private helper
+    return this.buildUrl(endpoint, params, v2Base);
+  }

Or extract a single private buildUrlWithBase(base, endpoint, params) helper that both buildUrl and buildV2Url delegate to.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/edd-client.ts` around lines 205 - 222, buildV2Url duplicates
authentication and param-iteration logic already present in buildUrl; refactor
to remove duplication by creating a shared helper or delegating: either add an
optional version parameter to buildUrl or implement a private helper like
buildUrlWithBase(base, endpoint, params) and have both buildUrl and buildV2Url
call that helper (move the base construction for v2 into the helper or compute
v2Base in buildV2Url and pass it to buildUrlWithBase), ensuring auth
(this.apiKey/this.apiToken) and the Object.entries params loop are only
implemented once.
tests/unit/edd-client.test.ts (1)

659-677: normalizeStatsResponse direct-format branch is not covered

All getStats test cases — including the new preset test — mock the wrapped { stats: { ... } } format. The fallback branch in normalizeStatsResponse (lines 488-489 in edd-client.ts):

return { [type]: response[type] } as StatsResponse['stats'];

has no test coverage. If the EDD API ever returns data directly (as the coding guideline suggests it should), this path would be exercised in production but never in CI.

✅ Suggested test to add
it('should handle direct (unwrapped) stats response format', async () => {
  mockFetch.mockResolvedValueOnce({
    ok: true,
    json: async () => ({
      earnings: { current_month: 3000, last_month: 2500, totals: 50000 },
      request_speed: 0.01,
    }),
  });

  const stats = await client.getStats('earnings');

  expect(stats.earnings?.current_month).toBe(3000);
  expect(stats.earnings?.totals).toBe(50000);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/edd-client.test.ts` around lines 659 - 677, Add a unit test
exercising the direct (unwrapped) response path in normalizeStatsResponse by
mocking fetch to return a JSON object that contains the stats fields at the top
level (e.g., { earnings: { current_month: 3000, totals: 50000 }, request_speed:
0.01 }) instead of wrapped under { stats: {...} }, then call
client.getStats('earnings') and assert the returned stats (for example that
stats.earnings.current_month === 3000 and stats.earnings.totals === 50000) so
the fallback branch in normalizeStatsResponse is covered; reference the existing
test scaffold (mockFetch, client.getStats, and normalizeStatsResponse) and add
this new it(...) case alongside the other getStats tests.
src/index.ts (1)

213-222: Move startDate/endDate pair validation to Zod schema

Per the coding guideline to use Zod schemas for input validation, the pair constraint should move from the handler to the input schema. MCP SDK v1.25.1's registerTool() accepts full ZodObject instances with .refine():

Schema-level validation using z.object().refine()
-    inputSchema: {
-      number: z.number().optional().describe('Number of customers to return (default: 10)'),
-      page: z.number().optional().describe('Page number for pagination'),
-      date: z.enum(['today', 'yesterday']).optional().describe('Filter by creation date preset'),
-      startDate: z.string().optional().describe('Start date in YYYYMMDD format (requires endDate)'),
-      endDate: z.string().optional().describe('End date in YYYYMMDD format (requires startDate)'),
-    },
+    inputSchema: z.object({
+      number: z.number().optional().describe('Number of customers to return (default: 10)'),
+      page: z.number().optional().describe('Page number for pagination'),
+      date: z.enum(['today', 'yesterday']).optional().describe('Filter by creation date preset'),
+      startDate: z.string().optional().describe('Start date in YYYYMMDD format (requires endDate)'),
+      endDate: z.string().optional().describe('End date in YYYYMMDD format (requires startDate)'),
+    }).refine(
+      (data) => !(data.startDate && !data.endDate) && !(!data.startDate && data.endDate),
+      { message: 'Both startDate and endDate are required when using date range filtering' }
+    ),

Remove the handler-level validation at lines 218–222 once the schema-level check is in place.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 213 - 222, Add the startDate/endDate pair
constraint into the Zod input schema by updating the z.object that defines
startDate and endDate to include .refine(data => (data.startDate &&
data.endDate) || (!data.startDate && !data.endDate), { message: 'Both startDate
and endDate are required when using date range filtering', path: ['startDate']
}) (or similar path), then remove the runtime check inside the handler async ({
number, page, date, startDate, endDate }) => that returns the error when only
one date is provided; keep the schema-level validation and rely on
registerTool() to surface validation errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/API.md`:
- Around line 549-551: The documented exponential backoff sequence is incorrect
for the described retry logic: with "retries = 3" and loop condition "attempt <=
retries" you only get two waits (1s after attempt 1 and 2s after attempt 2), so
change the docs or the control variables to match; either update the text under
"Retry Logic" to state the delays are (1s, 2s) for 3 total attempts, or if you
intend (1s, 2s, 4s) make the code use four attempts (e.g., set retries = 4 or
adjust the loop condition) so the backoff formula 2^(n-1)*1s produces 1s, 2s,
4s.

In `@src/edd-client.ts`:
- Around line 481-490: The normalizeStatsResponse function contains an
unreachable branch checking for a top-level "stats" wrapper; remove that branch
and simplify the implementation so it always returns the direct response shape
expected by the EDD API (i.e., return { [type]: response[type] } as
StatsResponse['stats']). Update normalizeStatsResponse to mirror how
getStatsByDateRange and getStatsByProduct handle the API response, ensuring no
"stats" wrapper logic remains.

---

Nitpick comments:
In `@src/edd-client.ts`:
- Around line 205-222: buildV2Url duplicates authentication and param-iteration
logic already present in buildUrl; refactor to remove duplication by creating a
shared helper or delegating: either add an optional version parameter to
buildUrl or implement a private helper like buildUrlWithBase(base, endpoint,
params) and have both buildUrl and buildV2Url call that helper (move the base
construction for v2 into the helper or compute v2Base in buildV2Url and pass it
to buildUrlWithBase), ensuring auth (this.apiKey/this.apiToken) and the
Object.entries params loop are only implemented once.

In `@src/index.ts`:
- Around line 213-222: Add the startDate/endDate pair constraint into the Zod
input schema by updating the z.object that defines startDate and endDate to
include .refine(data => (data.startDate && data.endDate) || (!data.startDate &&
!data.endDate), { message: 'Both startDate and endDate are required when using
date range filtering', path: ['startDate'] }) (or similar path), then remove the
runtime check inside the handler async ({ number, page, date, startDate, endDate
}) => that returns the error when only one date is provided; keep the
schema-level validation and rely on registerTool() to surface validation errors.

In `@tests/unit/edd-client.test.ts`:
- Around line 659-677: Add a unit test exercising the direct (unwrapped)
response path in normalizeStatsResponse by mocking fetch to return a JSON object
that contains the stats fields at the top level (e.g., { earnings: {
current_month: 3000, totals: 50000 }, request_speed: 0.01 }) instead of wrapped
under { stats: {...} }, then call client.getStats('earnings') and assert the
returned stats (for example that stats.earnings.current_month === 3000 and
stats.earnings.totals === 50000) so the fallback branch in
normalizeStatsResponse is covered; reference the existing test scaffold
(mockFetch, client.getStats, and normalizeStatsResponse) and add this new
it(...) case alongside the other getStats tests.

- Fix backoff docs: only 2 delays occur (1s, 2s), not 3 — third
  attempt either succeeds or throws
- Remove dead stats wrapper check from normalizeStatsResponse — EDD
  API returns stats directly ({ earnings: {...} }), never wrapped
- Update test mocks to match real EDD API response format

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@jack-arturo jack-arturo merged commit 44faa6d into verygoodplugins:main Feb 18, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants