Copilot commented on code in PR #8980:
URL: https://github.com/apache/devlake/pull/8980#discussion_r3573297938


##########
backend/plugins/gh-copilot/tasks/ai_credit_collector.go:
##########
@@ -0,0 +1,118 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "encoding/json"
+       "fmt"
+       "io"
+       "net/http"
+       "net/url"
+       "strconv"
+       "time"
+
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+)
+
+const rawAiCreditUsageTable = "copilot_ai_credit_usage"
+
+// CollectAiCreditUsage collects AI credit usage data from the billing API 
endpoints.
+// Routes requests to enterprise, organization, or user endpoints based on 
connection configuration.
+func CollectAiCreditUsage(taskCtx plugin.SubTaskContext) errors.Error {
+       data, ok := taskCtx.TaskContext().GetData().(*GhCopilotTaskData)
+       if !ok {
+               return errors.Default.New("task data is not GhCopilotTaskData")
+       }
+       connection := data.Connection
+       connection.Normalize()
+
+       var urlTemplate string
+       var scope string
+
+       if connection.HasEnterprise() {
+               urlTemplate = 
fmt.Sprintf("enterprises/%s/settings/billing/ai_credit/usage", 
connection.Enterprise)
+               scope = connection.Enterprise
+       } else if connection.Organization != "" {
+               urlTemplate = 
fmt.Sprintf("organizations/%s/settings/billing/ai_credit/usage", 
connection.Organization)
+               scope = connection.Organization
+       } else {
+               // User-level credits are scoped to current authenticated user
+               urlTemplate = "user/settings/billing/ai_credit/usage"
+               scope = "user"
+       }
+
+       // Build query parameters for date range
+       now := time.Now().UTC()
+       params := url.Values{}
+       params.Set("year", strconv.Itoa(now.Year()))
+       params.Set("month", strconv.Itoa(int(now.Month())))
+       params.Set("day", strconv.Itoa(now.Day()))
+
+       apiCollector, err := helper.NewApiCollector(taskCtx, 
rawAiCreditUsageTable)
+       if err != nil {
+               return err
+       }

Review Comment:
   This collector uses helper.NewApiCollector with a (taskCtx, table) 
signature, but the pluginhelper/api package only exposes 
NewApiCollector(ApiCollectorArgs). As written, this will not compile and also 
doesn’t provide an ApiClient (required by ApiCollectorArgs). Refactor to the 
same pattern used by other gh-copilot collectors: CreateApiClient(...) + 
NewStatefulApiCollector(rawArgs) + InitCollector(helper.ApiCollectorArgs{...}).



##########
backend/plugins/gh-copilot/tasks/ai_credit_collector.go:
##########
@@ -0,0 +1,118 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "encoding/json"
+       "fmt"
+       "io"
+       "net/http"
+       "net/url"
+       "strconv"
+       "time"
+
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+)
+
+const rawAiCreditUsageTable = "copilot_ai_credit_usage"
+
+// CollectAiCreditUsage collects AI credit usage data from the billing API 
endpoints.
+// Routes requests to enterprise, organization, or user endpoints based on 
connection configuration.
+func CollectAiCreditUsage(taskCtx plugin.SubTaskContext) errors.Error {
+       data, ok := taskCtx.TaskContext().GetData().(*GhCopilotTaskData)
+       if !ok {
+               return errors.Default.New("task data is not GhCopilotTaskData")
+       }
+       connection := data.Connection
+       connection.Normalize()
+
+       var urlTemplate string
+       var scope string
+
+       if connection.HasEnterprise() {
+               urlTemplate = 
fmt.Sprintf("enterprises/%s/settings/billing/ai_credit/usage", 
connection.Enterprise)
+               scope = connection.Enterprise
+       } else if connection.Organization != "" {
+               urlTemplate = 
fmt.Sprintf("organizations/%s/settings/billing/ai_credit/usage", 
connection.Organization)
+               scope = connection.Organization
+       } else {
+               // User-level credits are scoped to current authenticated user
+               urlTemplate = "user/settings/billing/ai_credit/usage"
+               scope = "user"
+       }
+
+       // Build query parameters for date range
+       now := time.Now().UTC()
+       params := url.Values{}
+       params.Set("year", strconv.Itoa(now.Year()))
+       params.Set("month", strconv.Itoa(int(now.Month())))
+       params.Set("day", strconv.Itoa(now.Day()))
+
+       apiCollector, err := helper.NewApiCollector(taskCtx, 
rawAiCreditUsageTable)
+       if err != nil {
+               return err
+       }
+
+       err = apiCollector.CollectPages(
+               func(col *helper.ApiCollector) errors.Error {
+                       col.GetAttr("scope", scope)
+                       col.SetRelation(fmt.Sprintf("%s-%s", 
data.Connection.ID, scope))
+                       col.SetQuery("pageSize", "100")
+                       for key, vals := range params {
+                               col.SetQuery(key, vals[0])
+                       }
+                       col.GetNextPageCustomerizedPath(fmt.Sprintf("%s?%s", 
urlTemplate, params.Encode()), nil)
+                       return nil

Review Comment:
   The helper.ApiCollector type does not provide 
GetAttr/SetRelation/SetQuery/GetNextPageCustomerizedPath/SaveRaw methods in 
helpers/pluginhelper/api. These calls will not compile. Use 
helper.ApiCollectorArgs (Query/ResponseParser/GetNextPageCustomData/etc.) with 
a stateful collector (as in seat_collector.go / user_metrics_collector.go) and 
return []json.RawMessage from ResponseParser instead of manually calling 
SaveRaw.



##########
backend/plugins/gh-copilot/tasks/ai_credit_extractor.go:
##########
@@ -0,0 +1,180 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "encoding/json"
+       "strconv"
+       "time"
+
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/gh-copilot/models"
+)
+
+// aiCreditUsageRecord represents a single usage item from the AI credit usage 
API.
+type aiCreditUsageRecord struct {
+       Product          string  `json:"product"`
+       Sku              string  `json:"sku"`
+       Model            string  `json:"model"`
+       UnitType         string  `json:"unitType"`
+       PricePerUnit     float64 `json:"pricePerUnit"`
+       GrossQuantity    float64 `json:"grossQuantity"`
+       DiscountQuantity float64 `json:"discountQuantity"`
+       NetQuantity      float64 `json:"netQuantity"`
+       GrossAmount      float64 `json:"grossAmount"`
+       DiscountAmount   float64 `json:"discountAmount"`
+       NetAmount        float64 `json:"netAmount"`
+}
+
+// aiCreditResponseWrapper represents the wrapper around the API response 
containing time period and usage items.
+type aiCreditResponseWrapper struct {
+       TimePeriod struct {
+               Year  int `json:"year"`
+               Month int `json:"month"`
+               Day   int `json:"day"`
+       } `json:"timePeriod"`
+       Enterprise   string `json:"enterprise"`
+       Organization string `json:"organization"`
+       User         string `json:"user"`
+       Product      string `json:"product"`
+       Model        string `json:"model"`
+       CostCenter   struct {
+               Id   string `json:"id"`
+               Name string `json:"name"`
+       } `json:"costCenter"`
+}
+
+// ExtractAiCreditUsage parses AI credit usage records into the appropriate 
model tables.
+func ExtractAiCreditUsage(taskCtx plugin.SubTaskContext) errors.Error {
+       data, ok := taskCtx.TaskContext().GetData().(*GhCopilotTaskData)
+       if !ok {
+               return errors.Default.New("task data is not GhCopilotTaskData")
+       }
+       connection := data.Connection
+       connection.Normalize()
+
+       extractor, err := helper.NewApiExtractor(taskCtx, rawAiCreditUsageTable)
+       if err != nil {
+               return err
+       }

Review Comment:
   This extractor is constructed with helper.NewApiExtractor(taskCtx, 
rawAiCreditUsageTable), but pluginhelper/api only exposes 
NewApiExtractor(ApiExtractorArgs). This will not compile. Follow the pattern 
used elsewhere (e.g. metrics_extractor.go): build helper.ApiExtractorArgs with 
RawDataSubTaskArgs{Ctx, Table, Options} and an Extract func, then call 
extractor.Execute().



##########
backend/plugins/gh-copilot/tasks/ai_credit_extractor.go:
##########
@@ -0,0 +1,180 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "encoding/json"
+       "strconv"
+       "time"
+
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/gh-copilot/models"
+)
+
+// aiCreditUsageRecord represents a single usage item from the AI credit usage 
API.
+type aiCreditUsageRecord struct {
+       Product          string  `json:"product"`
+       Sku              string  `json:"sku"`
+       Model            string  `json:"model"`
+       UnitType         string  `json:"unitType"`
+       PricePerUnit     float64 `json:"pricePerUnit"`
+       GrossQuantity    float64 `json:"grossQuantity"`
+       DiscountQuantity float64 `json:"discountQuantity"`
+       NetQuantity      float64 `json:"netQuantity"`
+       GrossAmount      float64 `json:"grossAmount"`
+       DiscountAmount   float64 `json:"discountAmount"`
+       NetAmount        float64 `json:"netAmount"`
+}
+
+// aiCreditResponseWrapper represents the wrapper around the API response 
containing time period and usage items.
+type aiCreditResponseWrapper struct {
+       TimePeriod struct {
+               Year  int `json:"year"`
+               Month int `json:"month"`
+               Day   int `json:"day"`
+       } `json:"timePeriod"`
+       Enterprise   string `json:"enterprise"`
+       Organization string `json:"organization"`
+       User         string `json:"user"`
+       Product      string `json:"product"`
+       Model        string `json:"model"`
+       CostCenter   struct {
+               Id   string `json:"id"`
+               Name string `json:"name"`
+       } `json:"costCenter"`
+}
+
+// ExtractAiCreditUsage parses AI credit usage records into the appropriate 
model tables.
+func ExtractAiCreditUsage(taskCtx plugin.SubTaskContext) errors.Error {
+       data, ok := taskCtx.TaskContext().GetData().(*GhCopilotTaskData)
+       if !ok {
+               return errors.Default.New("task data is not GhCopilotTaskData")
+       }
+       connection := data.Connection
+       connection.Normalize()
+
+       extractor, err := helper.NewApiExtractor(taskCtx, rawAiCreditUsageTable)
+       if err != nil {
+               return err
+       }
+
+       err = extractor.Extract(
+               rawAiCreditUsageTable,
+               func(row *helper.RawData) ([]interface{}, errors.Error) {
+                       // Parse raw data
+                       var record aiCreditUsageRecord
+                       err := json.Unmarshal(row.Data, &record)
+                       if err != nil {
+                               return nil, errors.Convert(err)
+                       }
+
+                       // Extract wrapper info from row context
+                       var wrapper aiCreditResponseWrapper
+                       if scopeAttr, ok := row.Params["scope"]; ok {
+                               if connection.HasEnterprise() {
+                                       wrapper.Enterprise = scopeAttr.(string)
+                               } else if connection.Organization != "" {
+                                       wrapper.Organization = 
scopeAttr.(string)
+                               }
+                       }
+                       wrapper.Product = record.Product
+                       wrapper.Model = record.Model
+
+                       // Parse date from row (need to extract from response 
context)
+                       // For now, use current date - this should be enhanced 
to parse from API response
+                       now := time.Now().UTC()
+                       wrapper.TimePeriod.Year = now.Year()
+                       wrapper.TimePeriod.Month = int(now.Month())
+                       wrapper.TimePeriod.Day = now.Day()
+

Review Comment:
   Using time.Now() to populate Year/Month/Day will make ingested records 
nondeterministic and mismatched with the billing API’s requested/returned time 
period (and will not match the E2E snapshots you added). Populate the time 
period from the request input/params (preferred) or from fields in the API 
response if present, rather than the current clock.



##########
backend/plugins/gh-copilot/tasks/ai_credit_extractor.go:
##########
@@ -0,0 +1,180 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "encoding/json"
+       "strconv"
+       "time"
+
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/gh-copilot/models"
+)

Review Comment:
   This file imports strconv but never uses it, which will fail compilation. 
Remove the unused import (or use it if intended).



##########
backend/plugins/gh-copilot/tasks/ai_credit_extractor.go:
##########
@@ -0,0 +1,180 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "encoding/json"
+       "strconv"
+       "time"
+
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/gh-copilot/models"
+)
+
+// aiCreditUsageRecord represents a single usage item from the AI credit usage 
API.
+type aiCreditUsageRecord struct {
+       Product          string  `json:"product"`
+       Sku              string  `json:"sku"`
+       Model            string  `json:"model"`
+       UnitType         string  `json:"unitType"`
+       PricePerUnit     float64 `json:"pricePerUnit"`
+       GrossQuantity    float64 `json:"grossQuantity"`
+       DiscountQuantity float64 `json:"discountQuantity"`
+       NetQuantity      float64 `json:"netQuantity"`
+       GrossAmount      float64 `json:"grossAmount"`
+       DiscountAmount   float64 `json:"discountAmount"`
+       NetAmount        float64 `json:"netAmount"`
+}
+
+// aiCreditResponseWrapper represents the wrapper around the API response 
containing time period and usage items.
+type aiCreditResponseWrapper struct {
+       TimePeriod struct {
+               Year  int `json:"year"`
+               Month int `json:"month"`
+               Day   int `json:"day"`
+       } `json:"timePeriod"`
+       Enterprise   string `json:"enterprise"`
+       Organization string `json:"organization"`
+       User         string `json:"user"`
+       Product      string `json:"product"`
+       Model        string `json:"model"`
+       CostCenter   struct {
+               Id   string `json:"id"`
+               Name string `json:"name"`
+       } `json:"costCenter"`
+}
+
+// ExtractAiCreditUsage parses AI credit usage records into the appropriate 
model tables.
+func ExtractAiCreditUsage(taskCtx plugin.SubTaskContext) errors.Error {
+       data, ok := taskCtx.TaskContext().GetData().(*GhCopilotTaskData)
+       if !ok {
+               return errors.Default.New("task data is not GhCopilotTaskData")
+       }
+       connection := data.Connection
+       connection.Normalize()
+
+       extractor, err := helper.NewApiExtractor(taskCtx, rawAiCreditUsageTable)
+       if err != nil {
+               return err
+       }
+
+       err = extractor.Extract(
+               rawAiCreditUsageTable,
+               func(row *helper.RawData) ([]interface{}, errors.Error) {
+                       // Parse raw data
+                       var record aiCreditUsageRecord
+                       err := json.Unmarshal(row.Data, &record)
+                       if err != nil {
+                               return nil, errors.Convert(err)
+                       }
+
+                       // Extract wrapper info from row context
+                       var wrapper aiCreditResponseWrapper
+                       if scopeAttr, ok := row.Params["scope"]; ok {
+                               if connection.HasEnterprise() {
+                                       wrapper.Enterprise = scopeAttr.(string)
+                               } else if connection.Organization != "" {
+                                       wrapper.Organization = 
scopeAttr.(string)
+                               }
+                       }

Review Comment:
   row.Params is a string (scope params JSON), not a map, so indexing 
row.Params["scope"] will not compile. If you need the enterprise/org identifier 
during extraction, either (a) carry it in RawDataSubTaskArgs.Options and 
unmarshal row.Params into the options struct, or (b) store it in RawData.Input 
by using a stateful collector Input iterator.



##########
backend/plugins/gh-copilot/tasks/ai_credit_extractor.go:
##########
@@ -0,0 +1,180 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "encoding/json"
+       "strconv"
+       "time"
+
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/gh-copilot/models"
+)
+
+// aiCreditUsageRecord represents a single usage item from the AI credit usage 
API.
+type aiCreditUsageRecord struct {
+       Product          string  `json:"product"`
+       Sku              string  `json:"sku"`
+       Model            string  `json:"model"`
+       UnitType         string  `json:"unitType"`
+       PricePerUnit     float64 `json:"pricePerUnit"`
+       GrossQuantity    float64 `json:"grossQuantity"`
+       DiscountQuantity float64 `json:"discountQuantity"`
+       NetQuantity      float64 `json:"netQuantity"`
+       GrossAmount      float64 `json:"grossAmount"`
+       DiscountAmount   float64 `json:"discountAmount"`
+       NetAmount        float64 `json:"netAmount"`
+}
+
+// aiCreditResponseWrapper represents the wrapper around the API response 
containing time period and usage items.
+type aiCreditResponseWrapper struct {
+       TimePeriod struct {
+               Year  int `json:"year"`
+               Month int `json:"month"`
+               Day   int `json:"day"`
+       } `json:"timePeriod"`
+       Enterprise   string `json:"enterprise"`
+       Organization string `json:"organization"`
+       User         string `json:"user"`
+       Product      string `json:"product"`
+       Model        string `json:"model"`
+       CostCenter   struct {
+               Id   string `json:"id"`
+               Name string `json:"name"`
+       } `json:"costCenter"`
+}
+
+// ExtractAiCreditUsage parses AI credit usage records into the appropriate 
model tables.
+func ExtractAiCreditUsage(taskCtx plugin.SubTaskContext) errors.Error {
+       data, ok := taskCtx.TaskContext().GetData().(*GhCopilotTaskData)
+       if !ok {
+               return errors.Default.New("task data is not GhCopilotTaskData")
+       }
+       connection := data.Connection
+       connection.Normalize()
+
+       extractor, err := helper.NewApiExtractor(taskCtx, rawAiCreditUsageTable)
+       if err != nil {
+               return err
+       }
+
+       err = extractor.Extract(
+               rawAiCreditUsageTable,
+               func(row *helper.RawData) ([]interface{}, errors.Error) {
+                       // Parse raw data
+                       var record aiCreditUsageRecord
+                       err := json.Unmarshal(row.Data, &record)
+                       if err != nil {
+                               return nil, errors.Convert(err)
+                       }
+
+                       // Extract wrapper info from row context
+                       var wrapper aiCreditResponseWrapper
+                       if scopeAttr, ok := row.Params["scope"]; ok {
+                               if connection.HasEnterprise() {
+                                       wrapper.Enterprise = scopeAttr.(string)
+                               } else if connection.Organization != "" {
+                                       wrapper.Organization = 
scopeAttr.(string)
+                               }
+                       }
+                       wrapper.Product = record.Product
+                       wrapper.Model = record.Model
+
+                       // Parse date from row (need to extract from response 
context)
+                       // For now, use current date - this should be enhanced 
to parse from API response
+                       now := time.Now().UTC()
+                       wrapper.TimePeriod.Year = now.Year()
+                       wrapper.TimePeriod.Month = int(now.Month())
+                       wrapper.TimePeriod.Day = now.Day()
+
+                       var results []interface{}
+
+                       // Route to appropriate table based on connection type
+                       if connection.HasEnterprise() {
+                               toolRecord := 
&models.GhCopilotEnterpriseAiCreditUsage{
+                                       ConnectionId:     data.Connection.ID,
+                                       ScopeId:          data.Options.ScopeId,
+                                       Year:             
wrapper.TimePeriod.Year,
+                                       Month:            
wrapper.TimePeriod.Month,
+                                       Day:              
wrapper.TimePeriod.Day,
+                                       Enterprise:       wrapper.Enterprise,
+                                       Model:            record.Model,
+                                       Organization:     wrapper.Organization,
+                                       User:             wrapper.User,
+                                       Product:          record.Product,
+                                       CostCenterId:     wrapper.CostCenter.Id,
+                                       CostCenterName:   
wrapper.CostCenter.Name,
+                                       GrossQuantity:    record.GrossQuantity,
+                                       DiscountQuantity: 
record.DiscountQuantity,
+                                       NetQuantity:      record.NetQuantity,
+                                       PricePerUnit:     record.PricePerUnit,
+                                       GrossAmount:      record.GrossAmount,
+                                       DiscountAmount:   record.DiscountAmount,
+                                       NetAmount:        record.NetAmount,
+                               }
+                               results = append(results, toolRecord)
+                       } else if connection.Organization != "" {
+                               toolRecord := &models.GhCopilotOrgAiCreditUsage{
+                                       ConnectionId:     data.Connection.ID,
+                                       ScopeId:          data.Options.ScopeId,
+                                       Year:             
wrapper.TimePeriod.Year,
+                                       Month:            
wrapper.TimePeriod.Month,
+                                       Day:              
wrapper.TimePeriod.Day,
+                                       Organization:     wrapper.Organization,
+                                       Model:            record.Model,
+                                       User:             wrapper.User,
+                                       Product:          record.Product,
+                                       GrossQuantity:    record.GrossQuantity,
+                                       DiscountQuantity: 
record.DiscountQuantity,
+                                       NetQuantity:      record.NetQuantity,
+                                       PricePerUnit:     record.PricePerUnit,
+                                       GrossAmount:      record.GrossAmount,
+                                       DiscountAmount:   record.DiscountAmount,
+                                       NetAmount:        record.NetAmount,
+                               }
+                               results = append(results, toolRecord)
+                       } else {
+                               // User-level credits
+                               toolRecord := 
&models.GhCopilotUserAiCreditUsage{
+                                       ConnectionId:     data.Connection.ID,
+                                       ScopeId:          data.Options.ScopeId,
+                                       Year:             
wrapper.TimePeriod.Year,
+                                       Month:            
wrapper.TimePeriod.Month,
+                                       Day:              
wrapper.TimePeriod.Day,
+                                       User:             
connection.GetEmail(), // Use authenticated user
+                                       Model:            record.Model,
+                                       Product:          record.Product,

Review Comment:
   connection.GetEmail() is called here, but GhCopilotConnection (and its 
embedded BaseConnection) doesn’t define GetEmail(), so this won’t compile. 
Also, the model field is documented as a GitHub username. Store the username 
from the API payload (or from connection config if user-scoped) instead of 
calling a non-existent method.



##########
backend/plugins/gh-copilot/models/migrationscripts/20260708_add_ai_credit_usage_metrics.go:
##########
@@ -0,0 +1,45 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+       "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/errors"
+       
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+       "github.com/apache/incubator-devlake/helpers/migrationhelper"
+       "github.com/apache/incubator-devlake/plugins/gh-copilot/models"
+)

Review Comment:
   The import of core/models/migrationscripts/archived is unused, which will 
fail compilation. Remove it from the import block.



##########
backend/plugins/gh-copilot/models/user_ai_credit_usage.go:
##########
@@ -0,0 +1,52 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+       "github.com/apache/incubator-devlake/core/models/common"
+)
+
+// GhCopilotUserAiCreditUsage tracks AI credit consumption at the individual 
user level.
+// One row per time period per model per authenticated user.
+type GhCopilotUserAiCreditUsage struct {
+       ConnectionId uint64 `gorm:"primaryKey" json:"connectionId"`
+       ScopeId      string `gorm:"primaryKey;type:varchar(255)" json:"scopeId"`
+       Year         int    `gorm:"primaryKey" json:"year"`
+       Month        int    `gorm:"primaryKey" json:"month"`
+       Day          int    `gorm:"primaryKey" json:"day"`
+
+       User  string `gorm:"primaryKey;type:varchar(255)" json:"user" 
gorm:"comment:GitHub username"`
+       Model string `gorm:"primaryKey;type:varchar(255)" json:"model" 
gorm:"comment:AI model name (e.g., gpt-4.1)"`
+
+       Product string `gorm:"type:varchar(32)" json:"product" 
gorm:"comment:Product name (e.g., copilot)"`

Review Comment:
   These struct fields repeat the `gorm:` tag key twice (e.g., `gorm:"..." ... 
gorm:"comment:..."`). Go struct tags do not merge duplicate keys; the second 
`gorm:` value will be ignored by reflect-based parsers (so the comment won’t be 
applied). Combine the comment into the single `gorm` tag (or drop it).



##########
backend/plugins/gh-copilot/models/enterprise_ai_credit_usage.go:
##########
@@ -0,0 +1,56 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+       "github.com/apache/incubator-devlake/core/models/common"
+)
+
+// GhCopilotEnterpriseAiCreditUsage tracks AI credit consumption at the 
enterprise level.
+// One row per time period per model per entity (user, org, or cost center).
+type GhCopilotEnterpriseAiCreditUsage struct {
+       ConnectionId uint64 `gorm:"primaryKey" json:"connectionId"`
+       ScopeId      string `gorm:"primaryKey;type:varchar(255)" json:"scopeId"`
+       Year         int    `gorm:"primaryKey" json:"year"`
+       Month        int    `gorm:"primaryKey" json:"month"`
+       Day          int    `gorm:"primaryKey" json:"day"`
+
+       Enterprise   string `gorm:"primaryKey;type:varchar(255)" 
json:"enterprise" gorm:"comment:Enterprise slug"`
+       Model        string `gorm:"primaryKey;type:varchar(255)" json:"model" 
gorm:"comment:AI model name (e.g., gpt-4.1)"`
+       Organization string `gorm:"index;type:varchar(255)" json:"organization" 
gorm:"comment:Organization within enterprise, if specified"`
+       User         string `gorm:"index;type:varchar(255)" json:"user" 
gorm:"comment:Username, if specified"`
+

Review Comment:
   These fields repeat the `gorm:` tag key twice; reflect-based tag parsing 
will ignore the later `gorm:"comment:..."` part, so the comments won’t be 
applied. Combine the comment into the single `gorm` tag for each field.



##########
backend/plugins/gh-copilot/tasks/ai_credit_collector.go:
##########
@@ -0,0 +1,118 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "encoding/json"
+       "fmt"
+       "io"
+       "net/http"
+       "net/url"
+       "strconv"
+       "time"
+
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+)
+
+const rawAiCreditUsageTable = "copilot_ai_credit_usage"
+
+// CollectAiCreditUsage collects AI credit usage data from the billing API 
endpoints.
+// Routes requests to enterprise, organization, or user endpoints based on 
connection configuration.
+func CollectAiCreditUsage(taskCtx plugin.SubTaskContext) errors.Error {
+       data, ok := taskCtx.TaskContext().GetData().(*GhCopilotTaskData)
+       if !ok {
+               return errors.Default.New("task data is not GhCopilotTaskData")
+       }
+       connection := data.Connection
+       connection.Normalize()
+
+       var urlTemplate string
+       var scope string
+
+       if connection.HasEnterprise() {
+               urlTemplate = 
fmt.Sprintf("enterprises/%s/settings/billing/ai_credit/usage", 
connection.Enterprise)
+               scope = connection.Enterprise
+       } else if connection.Organization != "" {
+               urlTemplate = 
fmt.Sprintf("organizations/%s/settings/billing/ai_credit/usage", 
connection.Organization)
+               scope = connection.Organization
+       } else {
+               // User-level credits are scoped to current authenticated user
+               urlTemplate = "user/settings/billing/ai_credit/usage"
+               scope = "user"

Review Comment:
   The PR description lists billing endpoints under `/organizations/{org}` and 
`/users/{username}`, but this collector uses `organizations/%s/...` and 
`user/...`. This mismatch makes it unclear which API you intend to call, and 
may break requests depending on what GitHub actually supports. Please align the 
path templates with the intended/validated endpoints (and keep them consistent 
with the PR description and existing gh-copilot usage of `orgs/{org}` 
elsewhere).



##########
backend/plugins/gh-copilot/models/enterprise_ai_credit_usage.go:
##########
@@ -0,0 +1,56 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+       "github.com/apache/incubator-devlake/core/models/common"
+)
+
+// GhCopilotEnterpriseAiCreditUsage tracks AI credit consumption at the 
enterprise level.
+// One row per time period per model per entity (user, org, or cost center).
+type GhCopilotEnterpriseAiCreditUsage struct {
+       ConnectionId uint64 `gorm:"primaryKey" json:"connectionId"`
+       ScopeId      string `gorm:"primaryKey;type:varchar(255)" json:"scopeId"`
+       Year         int    `gorm:"primaryKey" json:"year"`
+       Month        int    `gorm:"primaryKey" json:"month"`
+       Day          int    `gorm:"primaryKey" json:"day"`
+
+       Enterprise   string `gorm:"primaryKey;type:varchar(255)" 
json:"enterprise" gorm:"comment:Enterprise slug"`
+       Model        string `gorm:"primaryKey;type:varchar(255)" json:"model" 
gorm:"comment:AI model name (e.g., gpt-4.1)"`
+       Organization string `gorm:"index;type:varchar(255)" json:"organization" 
gorm:"comment:Organization within enterprise, if specified"`
+       User         string `gorm:"index;type:varchar(255)" json:"user" 
gorm:"comment:Username, if specified"`
+
+       Product        string `gorm:"type:varchar(32)" json:"product" 
gorm:"comment:Product name (e.g., copilot)"`
+       CostCenterId   string `gorm:"index;type:varchar(255)" 
json:"costCenterId" gorm:"comment:Cost center identifier"`
+       CostCenterName string `gorm:"type:varchar(255)" json:"costCenterName" 
gorm:"comment:Cost center display name"`

Review Comment:
   These fields repeat the `gorm:` tag key twice (including Product/CostCenter 
fields). Combine the comment into the same `gorm` tag so the metadata isn’t 
dropped.



##########
backend/plugins/gh-copilot/tasks/ai_credit_collector.go:
##########
@@ -0,0 +1,118 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "encoding/json"
+       "fmt"
+       "io"
+       "net/http"
+       "net/url"
+       "strconv"
+       "time"
+
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+)
+
+const rawAiCreditUsageTable = "copilot_ai_credit_usage"
+
+// CollectAiCreditUsage collects AI credit usage data from the billing API 
endpoints.
+// Routes requests to enterprise, organization, or user endpoints based on 
connection configuration.
+func CollectAiCreditUsage(taskCtx plugin.SubTaskContext) errors.Error {
+       data, ok := taskCtx.TaskContext().GetData().(*GhCopilotTaskData)
+       if !ok {
+               return errors.Default.New("task data is not GhCopilotTaskData")
+       }
+       connection := data.Connection
+       connection.Normalize()
+
+       var urlTemplate string
+       var scope string
+
+       if connection.HasEnterprise() {
+               urlTemplate = 
fmt.Sprintf("enterprises/%s/settings/billing/ai_credit/usage", 
connection.Enterprise)
+               scope = connection.Enterprise
+       } else if connection.Organization != "" {
+               urlTemplate = 
fmt.Sprintf("organizations/%s/settings/billing/ai_credit/usage", 
connection.Organization)
+               scope = connection.Organization
+       } else {
+               // User-level credits are scoped to current authenticated user
+               urlTemplate = "user/settings/billing/ai_credit/usage"
+               scope = "user"
+       }
+
+       // Build query parameters for date range
+       now := time.Now().UTC()
+       params := url.Values{}
+       params.Set("year", strconv.Itoa(now.Year()))
+       params.Set("month", strconv.Itoa(int(now.Month())))
+       params.Set("day", strconv.Itoa(now.Day()))
+

Review Comment:
   This collector claims to build query parameters for a date range, but it 
always uses the current UTC day only. That prevents backfill and incremental 
collection patterns used elsewhere in this plugin (day iterator + 
collector.GetSince()) and won’t populate historical periods expected by the new 
tables/snapshots. Consider iterating over the desired day range (like 
CollectUserMetrics/CollectUserTeams) and passing the requested day in each 
request so extraction can persist the correct Year/Month/Day.



##########
backend/plugins/gh-copilot/models/org_ai_credit_usage.go:
##########
@@ -0,0 +1,53 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+       "github.com/apache/incubator-devlake/core/models/common"
+)
+
+// GhCopilotOrgAiCreditUsage tracks AI credit consumption at the organization 
level.
+// One row per time period per model per user (within the organization).
+type GhCopilotOrgAiCreditUsage struct {
+       ConnectionId uint64 `gorm:"primaryKey" json:"connectionId"`
+       ScopeId      string `gorm:"primaryKey;type:varchar(255)" json:"scopeId"`
+       Year         int    `gorm:"primaryKey" json:"year"`
+       Month        int    `gorm:"primaryKey" json:"month"`
+       Day          int    `gorm:"primaryKey" json:"day"`
+
+       Organization string `gorm:"primaryKey;type:varchar(255)" 
json:"organization" gorm:"comment:Organization name"`
+       Model        string `gorm:"primaryKey;type:varchar(255)" json:"model" 
gorm:"comment:AI model name (e.g., gpt-4.1)"`
+       User         string `gorm:"index;type:varchar(255)" json:"user" 
gorm:"comment:Username consuming the credits"`
+
+       Product string `gorm:"type:varchar(32)" json:"product" 
gorm:"comment:Product name (e.g., copilot)"`

Review Comment:
   These struct fields repeat the `gorm:` tag key twice (e.g., `gorm:"..." ... 
gorm:"comment:..."`). Only the first `gorm` tag will be read by 
reflect.StructTag.Get, so the comment metadata will be silently dropped. 
Combine everything into a single `gorm` tag per field.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to