Skip to content

Commit 857bf98

Browse files
Fix patent number routing in GetPatentInfoParsed
1 parent c1c9cc4 commit 857bf98

File tree

3 files changed

+190
-9
lines changed

3 files changed

+190
-9
lines changed

client_patent.go

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,30 @@ import (
44
"context"
55
"fmt"
66
"io"
7+
"strings"
78
"time"
89

910
"github.com/patent-dev/dpma-connect-plus/generated"
1011

1112
openapi_types "github.com/oapi-codegen/runtime/types"
1213
)
1314

15+
// isRegisteredNumber returns true if the input looks like a bare DPMA registered number
16+
// (digits only, no DE prefix, no kind code). The DPMA getRegisterInfo API requires
17+
// the full registered number including check digit (e.g., "100273629").
18+
func isRegisteredNumber(s string) bool {
19+
s = strings.TrimSpace(s)
20+
if s == "" {
21+
return false
22+
}
23+
for _, c := range s {
24+
if c < '0' || c > '9' {
25+
return false
26+
}
27+
}
28+
return true
29+
}
30+
1431
// SearchPatents executes a patent/utility model expert search query
1532
func (c *Client) SearchPatents(ctx context.Context, query string) ([]byte, error) {
1633
resp, err := c.generated.SearchPatentsWithResponse(ctx, query)
@@ -29,7 +46,7 @@ func (c *Client) GetPatentPublicationPDF(ctx context.Context, documentID string)
2946
return resourceResult(resp.Body, resp.StatusCode(), "patent publication", documentID, "failed to download PDF")
3047
}
3148

32-
// GetPatentInfo retrieves patent information by registered number
49+
// GetPatentInfo retrieves patent information by registered number (digits only, including check digit).
3350
func (c *Client) GetPatentInfo(ctx context.Context, registeredNumber string) ([]byte, error) {
3451
resp, err := c.generated.GetPatentInfoWithResponse(ctx, registeredNumber)
3552
if err != nil {
@@ -300,12 +317,20 @@ func (c *Client) SearchPatentsParsed(ctx context.Context, query string) (*Patent
300317
}
301318

302319
// GetPatentInfoParsed retrieves patent info and returns parsed bibliographic data.
303-
func (c *Client) GetPatentInfoParsed(ctx context.Context, registeredNumber string) (*PatentInfo, error) {
304-
data, err := c.GetPatentInfo(ctx, registeredNumber)
305-
if err != nil {
306-
return nil, err
307-
}
308-
return ParsePatentInfo(data)
320+
// Accepts either a bare registered number (e.g., "100273629") or a DE patent number
321+
// with country prefix and/or kind code (e.g., "DE10027362C2", "DE102019200907A1").
322+
// For non-registered numbers, it resolves via publication number search automatically.
323+
func (c *Client) GetPatentInfoParsed(ctx context.Context, patentNumber string) (*PatentInfo, error) {
324+
patentNumber = strings.TrimSpace(patentNumber)
325+
if isRegisteredNumber(patentNumber) {
326+
data, err := c.GetPatentInfo(ctx, patentNumber)
327+
if err != nil {
328+
return nil, err
329+
}
330+
return ParsePatentInfo(data)
331+
}
332+
// Not a bare registered number - resolve via publication number search
333+
return c.GetPatentInfoByPublicationNumber(ctx, patentNumber)
309334
}
310335

311336
// GetPatentInfoByPublicationNumber resolves a DE publication number (e.g. "DE102019200907A1")

client_patent_test.go

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,15 +231,92 @@ func TestGetPatentInfoParsed(t *testing.T) {
231231
}
232232
}
233233

234-
func TestGetPatentInfoParsed_NotFound(t *testing.T) {
234+
func TestIsRegisteredNumber(t *testing.T) {
235+
tests := []struct {
236+
input string
237+
want bool
238+
}{
239+
{"100273629", true},
240+
{"123", true},
241+
{"", false},
242+
{"DE123C", false},
243+
{"DE10027362", false},
244+
{"de123c", false},
245+
{"10027362B4", false},
246+
}
247+
for _, tt := range tests {
248+
t.Run(tt.input, func(t *testing.T) {
249+
got := isRegisteredNumber(tt.input)
250+
if got != tt.want {
251+
t.Errorf("isRegisteredNumber(%q) = %v, want %v", tt.input, got, tt.want)
252+
}
253+
})
254+
}
255+
}
256+
257+
func TestGetPatentInfoParsed_RegisteredNumber(t *testing.T) {
258+
// Bare registered number should call getRegisterInfo directly
259+
handler := func(w http.ResponseWriter, r *http.Request) {
260+
requireAuth(t, r)
261+
requirePath(t, r, "/DPMAregisterPatService/getRegisterInfo/100273629")
262+
w.WriteHeader(http.StatusOK)
263+
w.Write(patentInfoXML)
264+
}
265+
266+
server, client := setupMockServer(t, handler)
267+
defer server.Close()
268+
269+
result, err := client.GetPatentInfoParsed(context.Background(), "100273629")
270+
if err != nil {
271+
t.Fatalf("GetPatentInfoParsed error = %v", err)
272+
}
273+
if result.Title == "" {
274+
t.Error("Title is empty")
275+
}
276+
}
277+
278+
func TestGetPatentInfoParsed_PublicationNumber(t *testing.T) {
279+
// DE patent number with prefix should resolve via search
280+
reqCount := 0
281+
handler := func(w http.ResponseWriter, r *http.Request) {
282+
requireAuth(t, r)
283+
reqCount++
284+
w.WriteHeader(http.StatusOK)
285+
if reqCount == 1 {
286+
// First request: search by PN=DE10027362C2
287+
requirePath(t, r, "/DPMAregisterPatService/search/")
288+
w.Write(patentSearchXML)
289+
} else {
290+
// Second request: get info by registered number from search result
291+
requirePath(t, r, "/DPMAregisterPatService/getRegisterInfo/")
292+
w.Write(patentInfoXML)
293+
}
294+
}
295+
296+
server, client := setupMockServer(t, handler)
297+
defer server.Close()
298+
299+
result, err := client.GetPatentInfoParsed(context.Background(), "DE10027362C2")
300+
if err != nil {
301+
t.Fatalf("GetPatentInfoParsed error = %v", err)
302+
}
303+
if result.Title == "" {
304+
t.Error("Title is empty")
305+
}
306+
if reqCount != 2 {
307+
t.Errorf("expected 2 requests (search + info), got %d", reqCount)
308+
}
309+
}
310+
311+
func TestGetPatentInfoParsed_NotFound_RegisteredNumber(t *testing.T) {
235312
handler := func(w http.ResponseWriter, r *http.Request) {
236313
w.WriteHeader(http.StatusNotFound)
237314
}
238315

239316
server, client := setupMockServer(t, handler)
240317
defer server.Close()
241318

242-
_, err := client.GetPatentInfoParsed(context.Background(), "INVALID")
319+
_, err := client.GetPatentInfoParsed(context.Background(), "999999999")
243320
if err == nil {
244321
t.Fatal("expected error for 404")
245322
}
@@ -248,6 +325,25 @@ func TestGetPatentInfoParsed_NotFound(t *testing.T) {
248325
}
249326
}
250327

328+
func TestGetPatentInfoParsed_NotFound_PublicationNumber(t *testing.T) {
329+
handler := func(w http.ResponseWriter, r *http.Request) {
330+
w.WriteHeader(http.StatusOK)
331+
w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?><PatentHitList HitCount="0"/>`))
332+
}
333+
334+
server, client := setupMockServer(t, handler)
335+
defer server.Close()
336+
337+
_, err := client.GetPatentInfoParsed(context.Background(), "DE999999999X1")
338+
if err == nil {
339+
t.Fatal("expected error for no results")
340+
}
341+
var notFound *NotFoundError
342+
if !errors.As(err, &notFound) {
343+
t.Errorf("expected *NotFoundError, got %T: %v", err, err)
344+
}
345+
}
346+
251347
func TestGetPatentInfoByPublicationNumber(t *testing.T) {
252348
// Mock server returns search results for the first request,
253349
// then patent info for the second.

integration_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,3 +490,63 @@ func TestIntegration_DataNotAvailable(t *testing.T) {
490490

491491
t.Logf("Correctly received DataNotAvailableError for %d week %d", year, week)
492492
}
493+
494+
func TestIntegration_GetPatentInfoParsed_NormalizeFormats(t *testing.T) {
495+
client := getTestClient(t)
496+
ctx := context.Background()
497+
498+
tests := []struct {
499+
name string
500+
input string
501+
}{
502+
// 100273602 is DE10027362 - a known working patent
503+
{"bare registered number", "100273602"},
504+
{"with DE prefix (known patent)", "DE10027362"},
505+
{"with DE prefix and kind code C2", "DE10027362C2"},
506+
{"publication number A1", "DE102019200907A1"},
507+
{"lowercase", "de10027362c2"},
508+
}
509+
510+
for _, tt := range tests {
511+
t.Run(tt.name, func(t *testing.T) {
512+
info, err := client.GetPatentInfoParsed(ctx, tt.input)
513+
if err != nil {
514+
t.Fatalf("GetPatentInfoParsed(%q) error = %v", tt.input, err)
515+
}
516+
if info.Title == "" {
517+
t.Errorf("GetPatentInfoParsed(%q) returned empty title", tt.input)
518+
}
519+
t.Logf("GetPatentInfoParsed(%q) -> title=%q, type=%q", tt.input, info.Title, info.IPRightType)
520+
})
521+
}
522+
}
523+
524+
func TestIntegration_GetPatentInfoByPublicationNumber_OldPatents(t *testing.T) {
525+
client := getTestClient(t)
526+
ctx := context.Background()
527+
528+
tests := []struct {
529+
name string
530+
input string
531+
}{
532+
{"DE123C (publication kind)", "DE123C"},
533+
{"DE123A (application kind)", "DE123A"},
534+
{"search PN=DE000000000123A", "DE000000000123A"},
535+
}
536+
537+
for _, tt := range tests {
538+
t.Run(tt.name, func(t *testing.T) {
539+
info, err := client.GetPatentInfoByPublicationNumber(ctx, tt.input)
540+
if err != nil {
541+
var notFound *NotFoundError
542+
if errors.As(err, &notFound) {
543+
t.Logf("%s not found via publication number search", tt.input)
544+
return
545+
}
546+
t.Errorf("GetPatentInfoByPublicationNumber(%s) error = %v", tt.input, err)
547+
return
548+
}
549+
t.Logf("GetPatentInfoByPublicationNumber(%s) -> title=%q", tt.input, info.Title)
550+
})
551+
}
552+
}

0 commit comments

Comments
 (0)