diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d470606 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,36 @@ +# Repository Guidelines + +## Project Structure & Module Organization +Wapi.go targets Go 1.21. Core SDK packages live under `pkg/` (messaging, events, business), shared HTTP plumbing sits in `internal/`, and orchestration helpers live in `manager/`. Examples are in `examples/` for quick sandbox runs. Generated references belong in `docs/` with templates in `docs/templates`. Tests sit beside implementations as `*_test.go`. + +## Build, Test, and Development Commands +- `go build ./...`: compiles all packages to catch cross‑package breakage early. +- `go test ./...`: runs the standard test suite; narrow with `-run`. +- `make format`: invokes `go fmt ./...` to match canonical Go style. +- `make docs`: installs `gomarkdoc` if absent and regenerates `docs/api-reference/`. +- `go run ./examples/chat-bot` or `go run ./examples/http-backend-integration`: validate changes against sample flows. + +## Coding Style & Naming Conventions +- Idiomatic Go: tabs and `gofmt` output. +- Exported identifiers use PascalCase with package‑level doc comments. +- Unexported helpers use camelCase and remain package‑local. +- Files/dirs are lowercase with hyphens. Prefer narrow, WhatsApp‑specific structs and reuse constructors from `manager/` and `pkg/`. + +## Testing Guidelines +- Use the standard `testing` package with table‑driven `TestXxx` functions. +- Stub outbound HTTP by wrapping `internal/request_client` (no live endpoints). +- Cover serialization, validation, and webhook dispatch; ensure `go test ./...` is clean. + +## Commit & Pull Request Guidelines +- Conventional Commits per `COMMIT_CONVENTION.md`, e.g., `feat(messaging): add template sender`. +- Branch names: `type/topic` (e.g., `fix/webhook-validation`). +- PRs summarize behavior, link issues, attach payloads/screenshots for API changes, and mention any doc updates (`make docs`). + +## Documentation Workflow +- API references are generated artifacts. Update doc comments and templates, not generated files. +- After changes, run `make docs` and commit both source and generated markdown. + +## Security & Configuration Tips (Optional) +- Provide API tokens via environment or secret management; never commit credentials. +- Store webhook secrets securely and rotate regularly. + diff --git a/README.md b/README.md index e2b6c94..ad4bfb2 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Visit the documentation of the SDK [here](https://golang.wapikit.com) ## Status -Beta Version - This SDK is not stable right now. It is currently in beta version. Report issues [here](https://github.com/wapikit/wapi.go/issues). +Beta Version - This SDK is not stable right now. It is currently in beta version. Report issues [here](https://github.com/gTahidi/wapi.go/issues). This SDK is part of a technical suite built to support the WhatsApp Business Application Development ecosystem. This SDK also has a Node.js version, you can check it out [here](https://wapikit/wapi.js/js). @@ -40,7 +40,7 @@ This assumes you already have a working Go environment, if not please see `go get` _will always pull the latest tagged release from the master branch._ ```sh -go get github.com/wapikit/wapi.go +go get github.com/gTahidi/wapi.go ``` > Note: This SDK is not affiliated with the official WhatsApp Cloud API or does not act as any official solution provided the the Meta Inclusive Private Limited, this is just a open source SDK built for developers to support them in building whatsapp cloud api based chat bots easily. @@ -54,13 +54,13 @@ You can check out the example WhatsApp bot here. [Example Chatbot](./example-cha Import the package into your project. This repository has three packages exported: -- github.com/wapikit/wapi.go/components -- github.com/wapikit/wapi.go/wapi/wapi -- github.com/wapikit/wapi.go/wapi/business -- github.com/wapikit/wapi.go/wapi/events +- github.com/gTahidi/wapi.go/components +- github.com/gTahidi/wapi.go/wapi/wapi +- github.com/gTahidi/wapi.go/wapi/business +- github.com/gTahidi/wapi.go/wapi/events ```go -import "github.com/wapikit/wapi.go/wapi/wapi" +import "github.com/gTahidi/wapi.go/wapi/wapi" ``` Construct a new Wapi Client to access the managers in order to send messages and listen to incoming notifications. diff --git a/docs/api-reference/internal.mdx b/docs/api-reference/internal.mdx index 9e95b75..2dc375a 100644 --- a/docs/api-reference/internal.mdx +++ b/docs/api-reference/internal.mdx @@ -1,5 +1,5 @@ ```go -import "github.com/wapikit/wapi.go/internal" +import "github.com/gTahidi/wapi.go/internal" ``` diff --git a/docs/api-reference/manager.mdx b/docs/api-reference/manager.mdx index f3a6ded..92a78f9 100644 --- a/docs/api-reference/manager.mdx +++ b/docs/api-reference/manager.mdx @@ -1,5 +1,5 @@ ```go -import "github.com/wapikit/wapi.go/manager" +import "github.com/gTahidi/wapi.go/manager" ``` diff --git a/docs/api-reference/pkg/business.mdx b/docs/api-reference/pkg/business.mdx index 3892d5f..d034f15 100644 --- a/docs/api-reference/pkg/business.mdx +++ b/docs/api-reference/pkg/business.mdx @@ -1,5 +1,5 @@ ```go -import "github.com/wapikit/wapi.go/pkg/business" +import "github.com/gTahidi/wapi.go/pkg/business" ``` diff --git a/docs/api-reference/pkg/client.mdx b/docs/api-reference/pkg/client.mdx index a88cc17..4d2b111 100644 --- a/docs/api-reference/pkg/client.mdx +++ b/docs/api-reference/pkg/client.mdx @@ -1,5 +1,5 @@ ```go -import "github.com/wapikit/wapi.go/pkg/client" +import "github.com/gTahidi/wapi.go/pkg/client" ``` diff --git a/docs/api-reference/pkg/components.mdx b/docs/api-reference/pkg/components.mdx index f4640a7..13d17ad 100644 --- a/docs/api-reference/pkg/components.mdx +++ b/docs/api-reference/pkg/components.mdx @@ -1,5 +1,5 @@ ```go -import "github.com/wapikit/wapi.go/pkg/components" +import "github.com/gTahidi/wapi.go/pkg/components" ``` diff --git a/docs/api-reference/pkg/events.mdx b/docs/api-reference/pkg/events.mdx index b26e0fa..0a742ef 100644 --- a/docs/api-reference/pkg/events.mdx +++ b/docs/api-reference/pkg/events.mdx @@ -1,5 +1,5 @@ ```go -import "github.com/wapikit/wapi.go/pkg/events" +import "github.com/gTahidi/wapi.go/pkg/events" ``` diff --git a/docs/api-reference/pkg/messaging.mdx b/docs/api-reference/pkg/messaging.mdx index f47a887..6f0b595 100644 --- a/docs/api-reference/pkg/messaging.mdx +++ b/docs/api-reference/pkg/messaging.mdx @@ -1,5 +1,5 @@ ```go -import "github.com/wapikit/wapi.go/pkg/messaging" +import "github.com/gTahidi/wapi.go/pkg/messaging" ``` diff --git a/docs/guide/building-your-application/building-message-components.mdx b/docs/guide/building-your-application/building-message-components.mdx index f174fa6..d4e6c62 100644 --- a/docs/guide/building-your-application/building-message-components.mdx +++ b/docs/guide/building-your-application/building-message-components.mdx @@ -22,7 +22,7 @@ Wapi.go SDK provides a simaple and easy to use classes architecture to build mes In all the media messages which includes image, document, audio, video and sticker, either we will have to use the Id of media, or a publicly accessible hosted media Url. - With the rapidly changing whatsapp business platform features and offerings, we try our best to be in sync with the new features provided by the API. If you think we are missing upon any of the available type of message support. You can open a github issue [here](https://github.com/wapikit/wapi.go/issues) or direclty [contact](/guide/contact) the maintainer of the project. + With the rapidly changing whatsapp business platform features and offerings, we try our best to be in sync with the new features provided by the API. If you think we are missing upon any of the available type of message support. You can open a github issue [here](https://github.com/gTahidi/wapi.go/issues) or direclty [contact](/guide/contact) the maintainer of the project. ### Text Message diff --git a/docs/guide/installation-and-preparations/creating-application.mdx b/docs/guide/installation-and-preparations/creating-application.mdx index 1aabffb..da9f3f5 100644 --- a/docs/guide/installation-and-preparations/creating-application.mdx +++ b/docs/guide/installation-and-preparations/creating-application.mdx @@ -11,7 +11,7 @@ The first step to build a Wapi.go based chat bot is to create a new project. You ```go go mod init -go get github.com/wapikit/wapi.go +go get github.com/gTahidi/wapi.go ``` ### Other use cases diff --git a/docs/guide/quickstart.mdx b/docs/guide/quickstart.mdx index c144d77..360bbd4 100644 --- a/docs/guide/quickstart.mdx +++ b/docs/guide/quickstart.mdx @@ -59,7 +59,7 @@ description: 'Welcome to the home of your new documentation' Check out the example chat bot to get inspiration diff --git a/docs/mint.json b/docs/mint.json index 692ee35..ad493b6 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -34,12 +34,12 @@ "topbarLinks": [ { "name": "Report Bugs", - "url": "https://github.com/wapikit/wapi.go/issues/new" + "url": "https://github.com/gTahidi/wapi.go/issues/new" } ], "topbarCtaButton": { "name": "Github", - "url": "https://github.com/wapikit/wapi.go" + "url": "https://github.com/gTahidi/wapi.go" }, "tabs": [ { @@ -51,7 +51,7 @@ { "name": "Star us on GitHub", "icon": "github", - "url": "https://github.com/wapikit/wapi.go" + "url": "https://github.com/gTahidi/wapi.go" }, { "name": "Sign up for WapiKit", @@ -122,7 +122,7 @@ ], "footerSocials": { "x": "https://x.com/wapikit", - "github": "https://github.com/wapikit/wapi.go", + "github": "https://github.com/gTahidi/wapi.go", "linkedin": "https://www.linkedin.com/company/wapikit" } } diff --git a/examples/chat-bot/main.go b/examples/chat-bot/main.go index 0cc55a7..b0f341a 100644 --- a/examples/chat-bot/main.go +++ b/examples/chat-bot/main.go @@ -5,10 +5,10 @@ import ( "strings" "time" - "github.com/wapikit/wapi.go/pkg/business" - wapi "github.com/wapikit/wapi.go/pkg/client" - wapiComponents "github.com/wapikit/wapi.go/pkg/components" - "github.com/wapikit/wapi.go/pkg/events" + "github.com/gTahidi/wapi.go/pkg/business" + wapi "github.com/gTahidi/wapi.go/pkg/client" + wapiComponents "github.com/gTahidi/wapi.go/pkg/components" + "github.com/gTahidi/wapi.go/pkg/events" ) func main() { diff --git a/examples/http-backend-integration/main.go b/examples/http-backend-integration/main.go index 16b6584..78e0de9 100644 --- a/examples/http-backend-integration/main.go +++ b/examples/http-backend-integration/main.go @@ -4,9 +4,9 @@ import ( "fmt" "github.com/labstack/echo/v4" - wapi "github.com/wapikit/wapi.go/pkg/client" - "github.com/wapikit/wapi.go/pkg/components" - "github.com/wapikit/wapi.go/pkg/events" + wapi "github.com/gTahidi/wapi.go/pkg/client" + "github.com/gTahidi/wapi.go/pkg/components" + "github.com/gTahidi/wapi.go/pkg/events" ) func main() { diff --git a/go.mod b/go.mod index 6764d23..acb817c 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/wapikit/wapi.go +module github.com/gTahidi/wapi.go go 1.21.3 diff --git a/manager/catalog_manager.go b/manager/catalog_manager.go index 913091b..c64e384 100644 --- a/manager/catalog_manager.go +++ b/manager/catalog_manager.go @@ -1,13 +1,18 @@ package manager import ( + "bytes" "encoding/json" "fmt" + "io" + "mime/multipart" "net/http" + "net/textproto" + "path/filepath" "strings" - "github.com/wapikit/wapi.go/internal" - "github.com/wapikit/wapi.go/internal/request_client" + "github.com/gTahidi/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal/request_client" ) type CatalogManager struct { @@ -63,6 +68,58 @@ type ProductFeed struct { Name string `json:"name"` } +// FeedUpload represents a single upload attempt and its status/diagnostics. +type FeedUpload struct { + Id string `json:"id,omitempty"` + Status string `json:"status,omitempty"` + Errors []string `json:"errors,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` + CreatedTime string `json:"created_time,omitempty"` + LastUpdated string `json:"last_updated_time,omitempty"` +} + +// FeedUploadResponse wraps basic responses for CSV upload operations. +type FeedUploadResponse struct { + Id string `json:"id,omitempty"` + Status string `json:"status,omitempty"` +} + +// FeedUploadSession represents an upload session listing entry (start/end time). +type FeedUploadSession struct { + Id string `json:"id"` + StartTime string `json:"start_time,omitempty"` + EndTime string `json:"end_time,omitempty"` +} + +// FeedUploadErrorSample represents a row sample in an error entry. +type FeedUploadErrorSample struct { + RowNumber int `json:"row_number"` + RetailerId string `json:"retailer_id"` + Id string `json:"id"` +} + +// FeedUploadError represents an individual ingestion error/warning. +type FeedUploadError struct { + Id string `json:"id"` + Summary string `json:"summary"` + Description string `json:"description"` + Severity string `json:"severity"` // fatal | warning + Samples struct { + Data []FeedUploadErrorSample `json:"data"` + } `json:"samples"` +} + +// FeedUploadErrorReport represents the generated error report metadata. +type FeedUploadErrorReport struct { + ReportStatus string `json:"report_status,omitempty"` + FileHandle string `json:"file_handle,omitempty"` +} + +type FeedUploadErrorReportResponse struct { + ErrorReport FeedUploadErrorReport `json:"error_report,omitempty"` + Id string `json:"id,omitempty"` +} + type ProductError struct { ErrorType string `json:"error_type"` ErrorPriority string `json:"error_priority"` @@ -344,7 +401,301 @@ type CreateProductCatalogOptions struct { func (cm *CatalogManager) CreateNewProductCatalog() (CreateProductCatalogOptions, error) { apiRequest := cm.requester.NewApiRequest(strings.Join([]string{cm.businessAccountId, "product_catalogs"}, "/"), http.MethodPost) response, err := apiRequest.Execute() + if err != nil { + // Return immediately on execution error; do not attempt to decode + return CreateProductCatalogOptions{}, fmt.Errorf("create catalog request failed: %w", err) + } var responseToReturn CreateProductCatalogOptions - json.Unmarshal([]byte(response), &responseToReturn) - return responseToReturn, err + if err := json.Unmarshal([]byte(response), &responseToReturn); err != nil { + // Return zero-value options with decoding context for callers + return CreateProductCatalogOptions{}, fmt.Errorf("decode create catalog response failed: %w", err) + } + return responseToReturn, nil +} + +// ListProductFeeds lists product feeds for a given catalog. +func (cm *CatalogManager) ListProductFeeds(catalogId string) ([]ProductFeed, error) { + apiPath := strings.Join([]string{catalogId, "product_feeds"}, "/") + apiRequest := cm.requester.NewApiRequest(apiPath, http.MethodGet) + response, err := apiRequest.Execute() + if err != nil { + return nil, err + } + var res struct { + Data []ProductFeed `json:"data"` + } + if err := json.Unmarshal([]byte(response), &res); err != nil { + return nil, err + } + return res.Data, nil +} + +// UploadFeedCSV uploads a CSV file to a product feed using multipart/form-data. +func (cm *CatalogManager) UploadFeedCSV(feedId string, file io.Reader, filename, mimeType string, updateOnly bool) (*FeedUploadResponse, error) { + // Prepare multipart body with update_only and a single file part + bodyBuf := new(bytes.Buffer) + writer := multipart.NewWriter(bodyBuf) + // update_only as string field + if err := writer.WriteField("update_only", func() string { + if updateOnly { + return "true" + } + return "false" + }()); err != nil { + return nil, fmt.Errorf("failed to write update_only: %w", err) + } + // file part + partHeader := make(textproto.MIMEHeader) + partHeader.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, filepath.Base(filename))) + partHeader.Set("Content-Type", mimeType) + filePart, err := writer.CreatePart(partHeader) + if err != nil { + return nil, fmt.Errorf("failed to create multipart part: %w", err) + } + if _, err := io.Copy(filePart, file); err != nil { + return nil, fmt.Errorf("failed to copy csv into part: %w", err) + } + if err := writer.Close(); err != nil { + return nil, fmt.Errorf("failed to close writer: %w", err) + } + + apiPath := strings.Join([]string{feedId, "uploads"}, "/") + contentType := writer.FormDataContentType() + responseBody, err := cm.requester.RequestMultipart(http.MethodPost, apiPath, bodyBuf, contentType) + if err != nil { + return nil, fmt.Errorf("error uploading CSV: %w", err) + } + + var res FeedUploadResponse + if err := json.Unmarshal([]byte(responseBody), &res); err != nil { + return nil, fmt.Errorf("failed to parse upload response: %w", err) + } + return &res, nil +} + +// UploadFeedCSVFromURL triggers a feed ingestion from a hosted CSV URL. +func (cm *CatalogManager) UploadFeedCSVFromURL(feedId, csvURL string, updateOnly bool) (*FeedUploadResponse, error) { + apiPath := strings.Join([]string{feedId, "uploads"}, "/") + apiRequest := cm.requester.NewApiRequest(apiPath, http.MethodPost) + body := map[string]interface{}{ + // Meta docs show using 'url' for hosted feed uploads + "url": csvURL, + "update_only": updateOnly, + } + payload, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to marshal hosted feed body: %w", err) + } + apiRequest.SetBody(string(payload)) + response, err := apiRequest.Execute() + if err != nil { + return nil, err + } + var res FeedUploadResponse + if err := json.Unmarshal([]byte(response), &res); err != nil { + return nil, err + } + return &res, nil +} + +// ListFeedUploads lists uploads for a product feed. +func (cm *CatalogManager) ListFeedUploads(feedId string) ([]FeedUploadSession, error) { + apiPath := strings.Join([]string{feedId, "uploads"}, "/") + apiRequest := cm.requester.NewApiRequest(apiPath, http.MethodGet) + response, err := apiRequest.Execute() + if err != nil { + return nil, err + } + var res struct { + Data []FeedUploadSession `json:"data"` + } + if err := json.Unmarshal([]byte(response), &res); err != nil { + return nil, err + } + return res.Data, nil +} + +// GetFeedUploadStatus fetches a single upload’s status/diagnostics. +func (cm *CatalogManager) GetFeedUploadStatus(uploadId string) (*FeedUploadErrorReportResponse, error) { + apiRequest := cm.requester.NewApiRequest(uploadId, http.MethodGet) + // include error_report field for convenience + apiRequest.AddField(request_client.ApiRequestQueryParamField{Name: "error_report", Filters: map[string]string{}}) + response, err := apiRequest.Execute() + if err != nil { + return nil, err + } + var res FeedUploadErrorReportResponse + if err := json.Unmarshal([]byte(response), &res); err != nil { + return nil, err + } + return &res, nil +} + +// GetFeedUploadErrors fetches a sampling of errors/warnings for an upload session. +func (cm *CatalogManager) GetFeedUploadErrors(uploadId string) ([]FeedUploadError, error) { + apiPath := strings.Join([]string{uploadId, "errors"}, "/") + apiRequest := cm.requester.NewApiRequest(apiPath, http.MethodGet) + response, err := apiRequest.Execute() + if err != nil { + return nil, err + } + var res struct { + Data []FeedUploadError `json:"data"` + } + if err := json.Unmarshal([]byte(response), &res); err != nil { + return nil, err + } + return res.Data, nil +} + +// RequestFeedUploadErrorReport triggers generation of a full error report. +func (cm *CatalogManager) RequestFeedUploadErrorReport(uploadId string) (bool, error) { + apiPath := strings.Join([]string{uploadId, "error_report"}, "/") + apiRequest := cm.requester.NewApiRequest(apiPath, http.MethodPost) + response, err := apiRequest.Execute() + if err != nil { + return false, err + } + var res struct { + Success bool `json:"success"` + } + if err := json.Unmarshal([]byte(response), &res); err != nil { + return false, err + } + return res.Success, nil +} + +// GetFeedUploadErrorReport fetches the error_report field of an upload session. +func (cm *CatalogManager) GetFeedUploadErrorReport(uploadId string) (*FeedUploadErrorReportResponse, error) { + apiRequest := cm.requester.NewApiRequest(uploadId, http.MethodGet) + apiRequest.AddField(request_client.ApiRequestQueryParamField{Name: "error_report", Filters: map[string]string{}}) + response, err := apiRequest.Execute() + if err != nil { + return nil, err + } + var res FeedUploadErrorReportResponse + if err := json.Unmarshal([]byte(response), &res); err != nil { + return nil, err + } + return &res, nil +} + +// ProductFeedSchedule represents schedule config for Google Sheets or hosted feeds. +type ProductFeedSchedule struct { + Url string `json:"url"` + Interval string `json:"interval"` // e.g., HOURLY, DAILY; Meta specific values + Hour *int `json:"hour,omitempty"` +} + +// CreateScheduledProductFeed creates a scheduled feed that fetches from a URL (Google Sheets supported via shareable link). +// When updateOnly is true, the feed behaves in update-only mode. +// ingestionSourceType: "PRIMARY_FEED" or "SUPPLEMENTARY_FEED" (optional) +// primaryFeedIds required for Supplementary feeds. +func (cm *CatalogManager) CreateScheduledProductFeed( + catalogId string, + name string, + schedule ProductFeedSchedule, + updateOnly bool, + ingestionSourceType string, + primaryFeedIds []string, +) (*ProductFeed, error) { + apiPath := strings.Join([]string{catalogId, "product_feeds"}, "/") + apiRequest := cm.requester.NewApiRequest(apiPath, http.MethodPost) + body := map[string]interface{}{ + "name": name, + "schedule": schedule, + "update_only": updateOnly, + } + if ingestionSourceType != "" { + body["ingestion_source_type"] = ingestionSourceType + } + if len(primaryFeedIds) > 0 { + body["primary_feed_ids"] = primaryFeedIds + } + payload, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to marshal scheduled feed body: %w", err) + } + apiRequest.SetBody(string(payload)) + response, err := apiRequest.Execute() + if err != nil { + return nil, err + } + var res ProductFeed + if err := json.Unmarshal([]byte(response), &res); err != nil { + return nil, err + } + return &res, nil +} + +// CreateProductFeed creates a product feed without a schedule (for immediate CSV uploads). +// Use UploadFeedCSV or UploadFeedCSVFromURL afterwards to ingest data. +func (cm *CatalogManager) CreateProductFeed(catalogId string, name string) (*ProductFeed, error) { + apiPath := strings.Join([]string{catalogId, "product_feeds"}, "/") + apiRequest := cm.requester.NewApiRequest(apiPath, http.MethodPost) + body := map[string]interface{}{ + "name": name, + } + payload, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to marshal feed body: %w", err) + } + apiRequest.SetBody(string(payload)) + response, err := apiRequest.Execute() + if err != nil { + return nil, err + } + var res ProductFeed + if err := json.Unmarshal([]byte(response), &res); err != nil { + return nil, err + } + return &res, nil +} + +// UpsertProductItem updates or creates a product item using Meta’s format. +// fields should include at least retailer_id, name, price, currency, image_url, availability, etc. +func (cm *CatalogManager) UpsertProductItem(catalogId string, fields map[string]interface{}) (*ProductItem, error) { + apiPath := strings.Join([]string{catalogId, "products"}, "/") + apiRequest := cm.requester.NewApiRequest(apiPath, http.MethodPost) + payload, err := json.Marshal(fields) + if err != nil { + return nil, fmt.Errorf("failed to marshall product fields: %w", err) + + } + apiRequest.SetBody(string(payload)) + response, err := apiRequest.Execute() + if err != nil { + return nil, err + } + var res ProductItem + if err := json.Unmarshal([]byte(response), &res); err != nil { + return nil, err + } + return &res, nil +} + +// BatchUpsertProductItems performs multiple upserts sequentially. +// Returns the successfully upserted items and a map of index->error for failures. +func (cm *CatalogManager) BatchUpsertProductItems(catalogId string, items []map[string]interface{}) ([]ProductItem, map[int]error) { + var results []ProductItem + errs := make(map[int]error) + for i, fields := range items { + item, err := cm.UpsertProductItem(catalogId, fields) + if err != nil { + errs[i] = err + continue + } + results = append(results, *item) + } + return results, errs +} + +// UpdateProductImages updates image_url and additional_image_urls for a retailer_id. +func (cm *CatalogManager) UpdateProductImages(catalogId, retailerId, imageURL string, additionalImageURLs []string) (*ProductItem, error) { + fields := map[string]interface{}{ + "retailer_id": retailerId, + "image_url": imageURL, + "additional_image_urls": additionalImageURLs, + } + return cm.UpsertProductItem(catalogId, fields) } diff --git a/manager/event_manager.go b/manager/event_manager.go index 9db4dca..28e3de8 100644 --- a/manager/event_manager.go +++ b/manager/event_manager.go @@ -4,7 +4,7 @@ import ( "fmt" "sync" - "github.com/wapikit/wapi.go/pkg/events" + "github.com/gTahidi/wapi.go/pkg/events" ) // ChannelEvent represents an event that can be published and subscribed to. diff --git a/manager/media_manager.go b/manager/media_manager.go index 0c32655..abd1925 100644 --- a/manager/media_manager.go +++ b/manager/media_manager.go @@ -11,7 +11,7 @@ import ( "path/filepath" "strings" - "github.com/wapikit/wapi.go/internal/request_client" + "github.com/gTahidi/wapi.go/internal/request_client" ) // MediaManager is responsible for managing media related operations. diff --git a/manager/message_manager.go b/manager/message_manager.go index ea41220..c4345e3 100644 --- a/manager/message_manager.go +++ b/manager/message_manager.go @@ -6,8 +6,8 @@ import ( "net/http" "strings" - "github.com/wapikit/wapi.go/internal/request_client" - "github.com/wapikit/wapi.go/pkg/components" + "github.com/gTahidi/wapi.go/internal/request_client" + "github.com/gTahidi/wapi.go/pkg/components" ) // MessageManager is responsible for managing messages. diff --git a/manager/phone_number_manager.go b/manager/phone_number_manager.go index c8d5c01..9758845 100644 --- a/manager/phone_number_manager.go +++ b/manager/phone_number_manager.go @@ -5,8 +5,8 @@ import ( "net/http" "strings" - "github.com/wapikit/wapi.go/internal" - "github.com/wapikit/wapi.go/internal/request_client" + "github.com/gTahidi/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal/request_client" ) // PhoneNumberManager is responsible for managing phone numbers for WhatsApp Business API and phone number specific operations. diff --git a/manager/template_manager.go b/manager/template_manager.go index df6b535..f331fa1 100644 --- a/manager/template_manager.go +++ b/manager/template_manager.go @@ -1,12 +1,13 @@ package manager import ( - "encoding/json" - "net/http" - "strings" + "encoding/json" + "fmt" + "net/http" + "strings" - "github.com/wapikit/wapi.go/internal" - "github.com/wapikit/wapi.go/internal/request_client" + "github.com/gTahidi/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal/request_client" ) // MessageTemplateStatus represents the status of a WhatsApp Business message template. @@ -232,11 +233,33 @@ type WhatsappMessageTemplateButtonCreateRequestBodyAlias = WhatsappMessageTempla // WhatsappMessageTemplateComponentCreateOrUpdateRequestBody represents the request body for creating/updating a component. type WhatsappMessageTemplateComponentCreateOrUpdateRequestBody struct { - Type MessageTemplateComponentType `json:"type,omitempty"` - Format MessageTemplateComponentFormat `json:"format,omitempty"` - Text string `json:"text,omitempty"` - Buttons []WhatsappMessageTemplateButtonCreateRequestBody `json:"buttons,omitempty"` - Example *TemplateMessageComponentExample `json:"example,omitempty"` + Type MessageTemplateComponentType `json:"type,omitempty"` + Format MessageTemplateComponentFormat `json:"format,omitempty"` + Text string `json:"text,omitempty"` + Buttons []WhatsappMessageTemplateButtonCreateRequestBody `json:"buttons,omitempty"` + Example *TemplateMessageComponentExample `json:"example,omitempty"` +} + +// validateCatalogAndMPMButtons ensures CATALOG and MPM buttons have required params +// and disallow unsupported fields. +func validateCatalogAndMPMButtons(components []WhatsappMessageTemplateComponentCreateOrUpdateRequestBody) error { + for _, c := range components { + if c.Type != MessageTemplateComponentTypeButtons { + continue + } + for _, b := range c.Buttons { + switch TemplateMessageButtonType(b.Type) { + case TemplateMessageButtonTypeCatalog, TemplateMessageButtonTypeMultiProductMessage: + if b.Text == "" { + return fmt.Errorf("template button of type %s requires non-empty text", b.Type) + } + // URL and phone_number are not relevant for catalog/mpm; ignore if present + default: + // no-op + } + } + } + return nil } // WhatsappMessageTemplateCreateRequestBody represents the request body for creating a message template. @@ -264,11 +287,15 @@ type MessageTemplateCreationResponse struct { // Create sends a creation request for a message template. func (manager *TemplateManager) Create(body WhatsappMessageTemplateCreateRequestBody) (*MessageTemplateCreationResponse, error) { - apiRequest := manager.requester.NewApiRequest(strings.Join([]string{manager.businessAccountId, "/", "message_templates"}, ""), http.MethodPost) - jsonBody, err := json.Marshal(body) - if err != nil { - return nil, err - } + // Pre-validate catalog and multi-product message buttons + if err := validateCatalogAndMPMButtons(body.Components); err != nil { + return nil, err + } + apiRequest := manager.requester.NewApiRequest(strings.Join([]string{manager.businessAccountId, "/", "message_templates"}, ""), http.MethodPost) + jsonBody, err := json.Marshal(body) + if err != nil { + return nil, err + } apiRequest.SetBody(string(jsonBody)) response, err := apiRequest.Execute() if err != nil { @@ -289,11 +316,17 @@ type WhatsAppBusinessAccountMessageTemplateUpdateRequestBody struct { // Update sends an update request for a template. func (manager *TemplateManager) Update(templateId string, updates WhatsAppBusinessAccountMessageTemplateUpdateRequestBody) (*MessageTemplateCreationResponse, error) { - apiRequest := manager.requester.NewApiRequest(strings.Join([]string{templateId}, ""), http.MethodPost) - jsonBody, err := json.Marshal(updates) - if err != nil { - return nil, err - } + // Pre-validate catalog and multi-product message buttons + if len(updates.Components) > 0 { + if err := validateCatalogAndMPMButtons(updates.Components); err != nil { + return nil, err + } + } + apiRequest := manager.requester.NewApiRequest(strings.Join([]string{templateId}, ""), http.MethodPost) + jsonBody, err := json.Marshal(updates) + if err != nil { + return nil, err + } apiRequest.SetBody(string(jsonBody)) response, err := apiRequest.Execute() if err != nil { diff --git a/manager/webhook_manager.go b/manager/webhook_manager.go index e7c9c1c..6fe929f 100644 --- a/manager/webhook_manager.go +++ b/manager/webhook_manager.go @@ -12,10 +12,10 @@ import ( "time" "github.com/labstack/echo/v4" - "github.com/wapikit/wapi.go/internal" - "github.com/wapikit/wapi.go/internal/request_client" - "github.com/wapikit/wapi.go/pkg/components" - "github.com/wapikit/wapi.go/pkg/events" + "github.com/gTahidi/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal/request_client" + "github.com/gTahidi/wapi.go/pkg/components" + "github.com/gTahidi/wapi.go/pkg/events" ) // WebhookManager represents a manager for handling webhooks. diff --git a/pkg/business/client.go b/pkg/business/client.go index 95c8dc1..bf0f485 100644 --- a/pkg/business/client.go +++ b/pkg/business/client.go @@ -4,12 +4,12 @@ import ( "encoding/json" "fmt" "net/http" - "strings" + "time" - "github.com/wapikit/wapi.go/internal" - "github.com/wapikit/wapi.go/internal/request_client" - "github.com/wapikit/wapi.go/manager" + "github.com/gTahidi/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal/request_client" + "github.com/gTahidi/wapi.go/manager" ) // BusinessClient is responsible for managing business account related operations. @@ -151,15 +151,24 @@ func (client *BusinessClient) FetchAnalytics(options AccountAnalyticsOptions) (W analyticsField.AddFilter("granularity", string(options.Granularity)) if len(options.PhoneNumbers) > 0 { - // get specific phone numbers - analyticsField.AddFilter("phone_numbers", strings.Join(options.PhoneNumbers, ",")) + // Pass as JSON array literal per Graph API (e.g., ["123","456"]) + if b, err := json.Marshal(options.PhoneNumbers); err == nil { + analyticsField.AddFilter("phone_numbers", string(b)) + } else { + // Fallback to empty (all) + analyticsField.AddFilter("phone_numbers", "[]") + } } else { // get all phone numbers analyticsField.AddFilter("phone_numbers", "[]") } if len(options.CountryCodes) > 0 { - analyticsField.AddFilter("country_codes", strings.Join(options.CountryCodes, ",")) + if b, err := json.Marshal(options.CountryCodes); err == nil { + analyticsField.AddFilter("country_codes", string(b)) + } else { + analyticsField.AddFilter("country_codes", "[]") + } } else { // get all country codes analyticsField.AddFilter("country_codes", "[]") @@ -222,22 +231,23 @@ type ConversationAnalyticsOptions struct { Granularity ConversationAnalyticsGranularityType `json:"granularity" validate:"required"` PhoneNumbers []string `json:"phone_numbers,omitempty"` - ConversationCategory []ConversationCategoryType `json:"conversation_category,omitempty"` - ConversationTypes []ConversationCategoryType `json:"conversation_types,omitempty"` - ConversationDirection []ConversationDirection `json:"conversation_direction,omitempty"` + // Use plural filter names to align with Graph API + ConversationCategory []ConversationCategoryType `json:"conversation_categories,omitempty"` + ConversationTypes []ConversationType `json:"conversation_types,omitempty"` + ConversationDirection []ConversationDirection `json:"conversation_directions,omitempty"` Dimensions []ConversationDimensionType `json:"dimensions,omitempty"` } type WhatsAppConversationAnalyticsNode struct { - Start int `json:"start" validate:"required"` - End int `json:"end,omitempty" validate:"required"` - Conversation int `json:"conversation,omitempty"` - PhoneNumber string `json:"phone_number,omitempty"` - Country string `json:"country,omitempty"` - ConversationType string `json:"conversation_type,omitempty"` - ConversationDirection string `json:"conversation_direction,omitempty"` - ConversationCategory string `json:"conversation_category,omitempty"` - Cost int `json:"cost,omitempty"` + Start int `json:"start" validate:"required"` + End int `json:"end,omitempty" validate:"required"` + Conversation int `json:"conversation,omitempty"` + PhoneNumber string `json:"phone_number,omitempty"` + Country string `json:"country,omitempty"` + ConversationType string `json:"conversation_type,omitempty"` + ConversationDirection string `json:"conversation_direction,omitempty"` + ConversationCategory string `json:"conversation_category,omitempty"` + Cost float64 `json:"cost,omitempty"` } type WhatsAppConversationAnalyticsEdge struct { @@ -263,8 +273,12 @@ func (client *BusinessClient) ConversationAnalytics(options ConversationAnalytic analyticsField.AddFilter("granularity", string(options.Granularity)) if len(options.PhoneNumbers) > 0 { - // get specific phone numbers - analyticsField.AddFilter("phone_numbers", strings.Join(options.PhoneNumbers, ",")) + // Pass as JSON array literal per Graph API + if b, err := json.Marshal(options.PhoneNumbers); err == nil { + analyticsField.AddFilter("phone_numbers", string(b)) + } else { + analyticsField.AddFilter("phone_numbers", "[]") + } } else { // get all phone numbers analyticsField.AddFilter("phone_numbers", "[]") @@ -275,9 +289,13 @@ func (client *BusinessClient) ConversationAnalytics(options ConversationAnalytic for i, category := range options.ConversationCategory { categoryStrings[i] = string(category) } - analyticsField.AddFilter("conversation_category", strings.Join(categoryStrings, ",")) + if b, err := json.Marshal(categoryStrings); err == nil { + analyticsField.AddFilter("conversation_categories", string(b)) + } else { + analyticsField.AddFilter("conversation_categories", "[]") + } } else { - analyticsField.AddFilter("conversation_category", "[]") // Empty slice + analyticsField.AddFilter("conversation_categories", "[]") // Empty slice } if len(options.ConversationTypes) > 0 { @@ -285,7 +303,11 @@ func (client *BusinessClient) ConversationAnalytics(options ConversationAnalytic for i, ctype := range options.ConversationTypes { typeStrings[i] = string(ctype) } - analyticsField.AddFilter("conversation_types", strings.Join(typeStrings, ",")) + if b, err := json.Marshal(typeStrings); err == nil { + analyticsField.AddFilter("conversation_types", string(b)) + } else { + analyticsField.AddFilter("conversation_types", "[]") + } } else { analyticsField.AddFilter("conversation_types", "[]") // Empty slice } @@ -295,9 +317,13 @@ func (client *BusinessClient) ConversationAnalytics(options ConversationAnalytic for i, direction := range options.ConversationDirection { directionStrings[i] = string(direction) } - analyticsField.AddFilter("conversation_direction", strings.Join(directionStrings, ",")) + if b, err := json.Marshal(directionStrings); err == nil { + analyticsField.AddFilter("conversation_directions", string(b)) + } else { + analyticsField.AddFilter("conversation_directions", "[]") + } } else { - analyticsField.AddFilter("conversation_direction", "[]") // Empty slice + analyticsField.AddFilter("conversation_directions", "[]") // Empty slice } if len(options.Dimensions) > 0 { @@ -305,9 +331,12 @@ func (client *BusinessClient) ConversationAnalytics(options ConversationAnalytic for i, dim := range options.Dimensions { dimensionsStrings[i] = string(dim) } - analyticsField.AddFilter("dimensions", strings.Join(dimensionsStrings, ",")) + if b, err := json.Marshal(dimensionsStrings); err == nil { + analyticsField.AddFilter("dimensions", string(b)) + } else { + analyticsField.AddFilter("dimensions", "[]") + } } else { - // get all country codes analyticsField.AddFilter("dimensions", "[]") } diff --git a/pkg/client/client.go b/pkg/client/client.go index 4847fed..a48b73a 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -2,11 +2,11 @@ package wapi import ( "github.com/labstack/echo/v4" - "github.com/wapikit/wapi.go/internal/request_client" - "github.com/wapikit/wapi.go/manager" - "github.com/wapikit/wapi.go/pkg/business" - "github.com/wapikit/wapi.go/pkg/events" - "github.com/wapikit/wapi.go/pkg/messaging" + "github.com/gTahidi/wapi.go/internal/request_client" + "github.com/gTahidi/wapi.go/manager" + "github.com/gTahidi/wapi.go/pkg/business" + "github.com/gTahidi/wapi.go/pkg/events" + "github.com/gTahidi/wapi.go/pkg/messaging" ) type ClientConfig struct { diff --git a/pkg/components/audio_message.go b/pkg/components/audio_message.go index 8f658f3..ae4db51 100644 --- a/pkg/components/audio_message.go +++ b/pkg/components/audio_message.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/wapikit/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal" ) // AudioMessage represents an audio message. diff --git a/pkg/components/catalog_message.go b/pkg/components/catalog_message.go index 53c1276..77cb24d 100644 --- a/pkg/components/catalog_message.go +++ b/pkg/components/catalog_message.go @@ -1,10 +1,10 @@ package components import ( - "encoding/json" - "fmt" + "encoding/json" + "fmt" - "github.com/wapikit/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal" ) type CatalogMessageActionParameter struct { @@ -38,15 +38,21 @@ type CatalogMessage struct { } func NewCatalogMessage(name, thumbnailProductRetailerId string) (*CatalogMessage, error) { - return &CatalogMessage{ - Type: InteractiveMessageTypeCatalog, - Action: CatalogMessageAction{ - Name: name, - Parameters: CatalogMessageActionParameter{ - ThumbnailProductRetailerId: thumbnailProductRetailerId, - }, - }, - }, nil + if thumbnailProductRetailerId == "" { + return nil, fmt.Errorf("thumbnail_product_retailer_id is required for catalog_message") + } + if name == "" { + name = "catalog_message" + } + return &CatalogMessage{ + Type: InteractiveMessageTypeCatalog, + Action: CatalogMessageAction{ + Name: name, + Parameters: CatalogMessageActionParameter{ + ThumbnailProductRetailerId: thumbnailProductRetailerId, + }, + }, + }, nil } func (m *CatalogMessage) SetHeader(text string) { @@ -75,9 +81,13 @@ type CatalogMessageApiPayload struct { // ToJson converts the product message to JSON with the given configurations. func (m *CatalogMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { - if err := internal.GetValidator().Struct(configs); err != nil { - return nil, fmt.Errorf("error validating configs: %v", err) - } + if err := internal.GetValidator().Struct(configs); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + // Validate message structure and required fields as well + if err := internal.GetValidator().Struct(m); err != nil { + return nil, fmt.Errorf("error validating catalog message: %v", err) + } jsonData := CatalogMessageApiPayload{ BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, MessageTypeInteractive), @@ -90,7 +100,7 @@ func (m *CatalogMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]by } } - jsonToReturn, err := json.Marshal(jsonData) + jsonToReturn, err := json.Marshal(jsonData) if err != nil { return nil, fmt.Errorf("error marshalling json: %v", err) diff --git a/pkg/components/contact_message.go b/pkg/components/contact_message.go index d0f6414..1fec502 100644 --- a/pkg/components/contact_message.go +++ b/pkg/components/contact_message.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/wapikit/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal" ) type AddressType string diff --git a/pkg/components/cta_message.go b/pkg/components/cta_message.go index d3397b2..a217b5d 100644 --- a/pkg/components/cta_message.go +++ b/pkg/components/cta_message.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/wapikit/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal" ) type CallToAction struct { diff --git a/pkg/components/document_message.go b/pkg/components/document_message.go index 29b8fc2..7407857 100644 --- a/pkg/components/document_message.go +++ b/pkg/components/document_message.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/wapikit/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal" ) // DocumentMessage represents a document message. diff --git a/pkg/components/image_message.go b/pkg/components/image_message.go index d162dfe..afa65b6 100644 --- a/pkg/components/image_message.go +++ b/pkg/components/image_message.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/wapikit/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal" ) // ImageMessage represents a message with an image. diff --git a/pkg/components/list_message.go b/pkg/components/list_message.go index d42a6f8..c410bb6 100644 --- a/pkg/components/list_message.go +++ b/pkg/components/list_message.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/wapikit/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal" ) // ListSection represents a section in the list message. diff --git a/pkg/components/location_message.go b/pkg/components/location_message.go index 857d5d7..702dc4b 100644 --- a/pkg/components/location_message.go +++ b/pkg/components/location_message.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/wapikit/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal" ) // LocationMessage represents a location message with latitude, longitude, address, and name. diff --git a/pkg/components/location_request_message.go b/pkg/components/location_request_message.go index 30f8040..95410f1 100644 --- a/pkg/components/location_request_message.go +++ b/pkg/components/location_request_message.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/wapikit/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal" ) type locationMessageAction struct { diff --git a/pkg/components/product_list_message.go b/pkg/components/product_list_message.go index d99be18..8500414 100644 --- a/pkg/components/product_list_message.go +++ b/pkg/components/product_list_message.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/wapikit/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal" ) type Product struct { @@ -29,9 +29,8 @@ func (ps *ProductSection) AddProduct(product Product) { } type ProductListMessageAction struct { - Sections []ProductSection `json:"sections" validate:"required"` // minimum 1 and maximum 10 - CatalogId string `json:"catalog_id" validate:"required"` - ProductRetailerId string `json:"product_retailer_id" validate:"required"` + Sections []ProductSection `json:"sections" validate:"required"` // minimum 1 and maximum 10 + CatalogId string `json:"catalog_id" validate:"required"` } func (a *ProductListMessageAction) AddSection(section ProductSection) { @@ -54,17 +53,17 @@ const ( // ! TODO: support more header types type ProductListMessageHeader struct { - Type ProductListMessageHeaderType `json:"type" validate:"required"` - Text string `json:"text" validate:"required"` + Type ProductListMessageHeaderType `json:"type" validate:"required"` + Text string `json:"text" validate:"required"` } // ProductListMessage represents a product list message. type ProductListMessage struct { - Action ProductListMessageAction `json:"action" validate:"required"` - Body ProductListMessageBody `json:"body" validate:"required"` - Footer *ProductListMessageFooter `json:"footer,omitempty"` - Header ProductListMessageHeader `json:"header,omitempty"` - Type InteractiveMessageType `json:"type" validate:"required"` + Action ProductListMessageAction `json:"action" validate:"required"` + Body ProductListMessageBody `json:"body" validate:"required"` + Footer *ProductListMessageFooter `json:"footer,omitempty"` + Header *ProductListMessageHeader `json:"header,omitempty"` + Type InteractiveMessageType `json:"type" validate:"required"` } func (message *ProductListMessage) AddSection(section ProductSection) { @@ -82,8 +81,10 @@ func (message *ProductListMessage) SetCatalogId(catalogId string) { message.Action.CatalogId = catalogId } +// SetProductRetailerId is deprecated. Product retailer IDs belong to each item. +// This method is kept for backward compatibility and is now a no-op. func (message *ProductListMessage) SetProductRetailerId(productRetailerId string) { - message.Action.ProductRetailerId = productRetailerId + // no-op } func (message *ProductListMessage) SetFooter(text string) { @@ -93,18 +94,19 @@ func (message *ProductListMessage) SetFooter(text string) { } func (message *ProductListMessage) SetHeader(text string) { - message.Header = ProductListMessageHeader{ - Type: ProductListMessageHeaderTypeText, - Text: text, - } + message.Header = &ProductListMessageHeader{ + Type: ProductListMessageHeaderTypeText, + Text: text, + } } // ProductListMessageParams represents the parameters for creating a product list message. type ProductListMessageParams struct { - CatalogId string `validate:"required"` - ProductRetailerId string `validate:"required"` - BodyText string `validate:"required"` - Sections []ProductSection + CatalogId string `validate:"required"` + // Deprecated: action-level product_retailer_id. Use item-level IDs in sections. + ProductRetailerId string + BodyText string `validate:"required"` + Sections []ProductSection } // ProductListMessageApiPayload represents the API payload for a product list message. @@ -119,24 +121,36 @@ func NewProductListMessage(params ProductListMessageParams) (*ProductListMessage return nil, fmt.Errorf("error validating configs: %v", err) } - return &ProductListMessage{ - Type: InteractiveMessageTypeProductList, - Body: ProductListMessageBody{ - Text: params.BodyText, - }, - Action: ProductListMessageAction{ - CatalogId: params.CatalogId, - ProductRetailerId: params.ProductRetailerId, - Sections: params.Sections, - }, - }, nil + return &ProductListMessage{ + Type: InteractiveMessageTypeProductList, + Body: ProductListMessageBody{ + Text: params.BodyText, + }, + Action: ProductListMessageAction{ + CatalogId: params.CatalogId, + Sections: params.Sections, + }, + }, nil } // ToJson converts the product list message to JSON. func (m *ProductListMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { - if err := internal.GetValidator().Struct(configs); err != nil { - return nil, fmt.Errorf("error validating configs: %v", err) - } + if err := internal.GetValidator().Struct(configs); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + // Validate message structure and section/item limits + if err := internal.GetValidator().Struct(m); err != nil { + return nil, fmt.Errorf("error validating product list message: %v", err) + } + // Enforce Meta limits: ≤10 sections, ≤30 items per section + if len(m.Action.Sections) == 0 || len(m.Action.Sections) > 10 { + return nil, fmt.Errorf("product_list must contain between 1 and 10 sections") + } + for i, s := range m.Action.Sections { + if len(s.Products) == 0 || len(s.Products) > 30 { + return nil, fmt.Errorf("section %d must contain between 1 and 30 products", i) + } + } jsonData := ProductListMessageApiPayload{ BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, MessageTypeInteractive), diff --git a/pkg/components/product_message.go b/pkg/components/product_message.go index 886bb2f..8388a8d 100644 --- a/pkg/components/product_message.go +++ b/pkg/components/product_message.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/wapikit/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal" ) type ProductMessageBody struct { diff --git a/pkg/components/quick_reply_button_message.go b/pkg/components/quick_reply_button_message.go index b5ebb5d..af8644c 100644 --- a/pkg/components/quick_reply_button_message.go +++ b/pkg/components/quick_reply_button_message.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/wapikit/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal" ) // quickReplyButtonMessageButtonReply represents the reply structure of a quick reply button. diff --git a/pkg/components/reaction_message.go b/pkg/components/reaction_message.go index 6bf353c..ac37543 100644 --- a/pkg/components/reaction_message.go +++ b/pkg/components/reaction_message.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/wapikit/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal" ) // ReactionMessage represents a reaction to a message. diff --git a/pkg/components/sticker_message.go b/pkg/components/sticker_message.go index 111cb0d..8f003db 100644 --- a/pkg/components/sticker_message.go +++ b/pkg/components/sticker_message.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/wapikit/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal" ) // StickerMessage represents a sticker message. diff --git a/pkg/components/template_message.go b/pkg/components/template_message.go index 7169e22..d2d618c 100644 --- a/pkg/components/template_message.go +++ b/pkg/components/template_message.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/wapikit/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal" ) // TemplateMessageComponentType represents the type of a template message component. @@ -183,8 +183,11 @@ type TemplateMessageMultiProductButtonActionParameterSection struct { ProductItems []TemplateMessageMultiProductButtonActionParameterProductItem `json:"product_items" validate:"required"` } +// TemplateMessageButtonParameterAction represents the action parameters for catalog buttons. +// Per WhatsApp API docs, thumbnail_product_retailer_id is optional - if omitted, WhatsApp +// automatically uses the first product in the catalog as the thumbnail. type TemplateMessageButtonParameterAction struct { - ThumbnailProductRetailerId string `json:"thumbnail_product_retailer_id" validate:"required"` + ThumbnailProductRetailerId string `json:"thumbnail_product_retailer_id,omitempty"` Sections *[]TemplateMessageMultiProductButtonActionParameterSection `json:"sections,omitempty"` // Required for MPM buttons. UPTO 10 sections in a buttons parameter } @@ -324,5 +327,8 @@ func (m *TemplateMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]b return nil, fmt.Errorf("error marshalling json: %v", err) } + // Debug logging: print the actual JSON being sent + fmt.Printf("[DEBUG] Template Message JSON: %s\n", string(jsonToReturn)) + return jsonToReturn, nil } diff --git a/pkg/components/text_message.go b/pkg/components/text_message.go index 497f949..3d5eba0 100644 --- a/pkg/components/text_message.go +++ b/pkg/components/text_message.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/wapikit/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal" ) // textMessage represents a text message. diff --git a/pkg/components/video_message.go b/pkg/components/video_message.go index 94f82dc..4897760 100644 --- a/pkg/components/video_message.go +++ b/pkg/components/video_message.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/wapikit/wapi.go/internal" + "github.com/gTahidi/wapi.go/internal" ) // VideoMessage represents a video message. diff --git a/pkg/events/audio_message_event.go b/pkg/events/audio_message_event.go index dac9f14..95d1c94 100644 --- a/pkg/events/audio_message_event.go +++ b/pkg/events/audio_message_event.go @@ -1,7 +1,7 @@ package events import ( - "github.com/wapikit/wapi.go/pkg/components" + "github.com/gTahidi/wapi.go/pkg/components" ) // AudioMessageEvent represents an event for an audio message. diff --git a/pkg/events/base_event.go b/pkg/events/base_event.go index dcfc016..2eb4fca 100644 --- a/pkg/events/base_event.go +++ b/pkg/events/base_event.go @@ -4,8 +4,8 @@ import ( "net/http" "strings" - "github.com/wapikit/wapi.go/internal/request_client" - "github.com/wapikit/wapi.go/pkg/components" + "github.com/gTahidi/wapi.go/internal/request_client" + "github.com/gTahidi/wapi.go/pkg/components" ) type MessageContext struct { diff --git a/pkg/events/contacts_message_event.go b/pkg/events/contacts_message_event.go index 82c1ddd..5d685fa 100644 --- a/pkg/events/contacts_message_event.go +++ b/pkg/events/contacts_message_event.go @@ -1,6 +1,6 @@ package events -import "github.com/wapikit/wapi.go/pkg/components" +import "github.com/gTahidi/wapi.go/pkg/components" // ContactsMessageEvent represents an event that occurs when a message with contacts is received. type ContactsMessageEvent struct { diff --git a/pkg/events/document_message_event.go b/pkg/events/document_message_event.go index d1838a7..0afc5f0 100644 --- a/pkg/events/document_message_event.go +++ b/pkg/events/document_message_event.go @@ -1,6 +1,6 @@ package events -import "github.com/wapikit/wapi.go/pkg/components" +import "github.com/gTahidi/wapi.go/pkg/components" // DocumentMessageEvent represents an event that occurs when a document message is received. type DocumentMessageEvent struct { diff --git a/pkg/events/image_message_event.go b/pkg/events/image_message_event.go index 5f7e4df..9e8db02 100644 --- a/pkg/events/image_message_event.go +++ b/pkg/events/image_message_event.go @@ -1,6 +1,6 @@ package events -import "github.com/wapikit/wapi.go/pkg/components" +import "github.com/gTahidi/wapi.go/pkg/components" // ImageMessageEvent represents an event for an image message. type ImageMessageEvent struct { diff --git a/pkg/events/location_message_event.go b/pkg/events/location_message_event.go index 9b7e335..0d7eaca 100644 --- a/pkg/events/location_message_event.go +++ b/pkg/events/location_message_event.go @@ -1,6 +1,6 @@ package events -import "github.com/wapikit/wapi.go/pkg/components" +import "github.com/gTahidi/wapi.go/pkg/components" // LocationMessageEvent represents an event that contains a location message. type LocationMessageEvent struct { diff --git a/pkg/events/order_event.go b/pkg/events/order_event.go index 58b4f22..84da307 100644 --- a/pkg/events/order_event.go +++ b/pkg/events/order_event.go @@ -1,6 +1,6 @@ package events -import "github.com/wapikit/wapi.go/pkg/components" +import "github.com/gTahidi/wapi.go/pkg/components" // OrderEvent represents an event related to an order. type OrderEvent struct { diff --git a/pkg/events/reaction_event.go b/pkg/events/reaction_event.go index 5a0de47..89decf4 100644 --- a/pkg/events/reaction_event.go +++ b/pkg/events/reaction_event.go @@ -1,6 +1,6 @@ package events -import "github.com/wapikit/wapi.go/pkg/components" +import "github.com/gTahidi/wapi.go/pkg/components" // ReactionMessageEvent represents an event that occurs when a reaction is added to a message. type ReactionMessageEvent struct { diff --git a/pkg/events/sticker_message_event.go b/pkg/events/sticker_message_event.go index 7a671fc..be9f8f4 100644 --- a/pkg/events/sticker_message_event.go +++ b/pkg/events/sticker_message_event.go @@ -1,6 +1,6 @@ package events -import "github.com/wapikit/wapi.go/pkg/components" +import "github.com/gTahidi/wapi.go/pkg/components" // StickerMessageEvent represents an event for a sticker message. type StickerMessageEvent struct { diff --git a/pkg/events/video_message_event.go b/pkg/events/video_message_event.go index 9f06740..5712c86 100644 --- a/pkg/events/video_message_event.go +++ b/pkg/events/video_message_event.go @@ -1,6 +1,6 @@ package events -import "github.com/wapikit/wapi.go/pkg/components" +import "github.com/gTahidi/wapi.go/pkg/components" // VideoMessageEvent represents a WhatsApp video message event. type VideoMessageEvent struct { diff --git a/pkg/messaging/client.go b/pkg/messaging/client.go index eb68d94..3491f44 100644 --- a/pkg/messaging/client.go +++ b/pkg/messaging/client.go @@ -5,8 +5,8 @@ import ( "net/http" "strings" - "github.com/wapikit/wapi.go/internal/request_client" - "github.com/wapikit/wapi.go/manager" + "github.com/gTahidi/wapi.go/internal/request_client" + "github.com/gTahidi/wapi.go/manager" ) // MessagingClient represents a WhatsApp client.