Skip to content
60 changes: 60 additions & 0 deletions cmd/company.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ var (
companyFilingsTo string
companyFilingsLimit int
companySearchIndustry string

companyFinancialsPeriods int
companyFinancialsStatement string
companyFinancialsNonConsolidated bool
)

const (
Expand Down Expand Up @@ -121,6 +125,57 @@ var companyFilingsCmd = &cobra.Command{
},
}

var companyFinancialsCmd = &cobra.Command{
Use: "financials <code>",
Short: "Extract financial statements for multiple fiscal periods",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
code := args[0]

if companyFinancialsPeriods < 1 || companyFinancialsPeriods > 10 {
return &api.EDINETError{Code: api.ErrValidation, Message: fmt.Sprintf("--periods must be between 1 and 10, got %d", companyFinancialsPeriods)}
}
if err := validateStatement(companyFinancialsStatement); err != nil {
return err
}
if app.Config.SubscriptionKey == "" {
return &api.EDINETError{Code: api.ErrAuth, Message: "EDINET_API_KEY environment variable is required"}
}

client := api.NewClient(app.Config.SubscriptionKey, "https://api.edinet-fsa.go.jp", app.Config.Debug)
docSvc := service.NewDocumentService(client, app.Cache, cmd.ErrOrStderr())
finSvc := service.NewFinancialService(client, app.Cache)

var reg *company.Registry
if !isEdinetCode(code) {
var err error
reg, err = loadRegistry()
if err != nil {
return err
}
}

companySvc := service.NewCompanyService(reg, docSvc)

opts := service.CompanyFinancialsOpts{
StatementOpts: service.StatementOpts{
Statement: companyFinancialsStatement,
},
Periods: companyFinancialsPeriods,
RateLimit: 100 * time.Millisecond,
}
if companyFinancialsNonConsolidated {
opts.Consolidated = ptrBool(false)
}

result, err := finSvc.GetCompanyFinancials(cmd.Context(), companySvc, code, opts)
if err != nil {
return err
}
return outputResult(cmd.OutOrStdout(), result)
},
}

var companyUpdateCmd = &cobra.Command{
Use: "update",
Short: "Download and update the EDINET code list",
Expand Down Expand Up @@ -228,8 +283,13 @@ func init() {
companyFilingsCmd.Flags().StringVar(&companyFilingsTo, "to", "", "Range end date (default: today)")
companyFilingsCmd.Flags().IntVar(&companyFilingsLimit, "limit", 0, "Maximum number of results (0=unlimited)")

companyFinancialsCmd.Flags().IntVar(&companyFinancialsPeriods, "periods", 3, "Number of fiscal periods (1-10)")
companyFinancialsCmd.Flags().StringVar(&companyFinancialsStatement, "statement", "all", "Statement type: bs, pl, cf, all")
companyFinancialsCmd.Flags().BoolVar(&companyFinancialsNonConsolidated, "non-consolidated", false, "Prefer non-consolidated statements")

companyCmd.AddCommand(companySearchCmd)
companyCmd.AddCommand(companyFilingsCmd)
companyCmd.AddCommand(companyFinancialsCmd)
companyCmd.AddCommand(companyUpdateCmd)
rootCmd.AddCommand(companyCmd)
}
41 changes: 41 additions & 0 deletions cmd/company_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,44 @@ func TestCompanyFilings_HelpOutput(t *testing.T) {
t.Error("expected help output")
}
}

func TestCompanyFinancials_NoArgs(t *testing.T) {
_, _, code := executeCommand("company", "financials")
if code == 0 {
t.Error("expected non-zero exit code when no code provided")
}
}

func TestCompanyFinancials_PeriodsZero(t *testing.T) {
_, stderr, code := executeCommand("company", "financials", "E02144", "--periods", "0")
if code == 0 {
t.Error("expected non-zero exit code for --periods 0")
}
expectErrorCode(t, stderr, "VALIDATION_ERROR")
}

func TestCompanyFinancials_PeriodsTooHigh(t *testing.T) {
_, stderr, code := executeCommand("company", "financials", "E02144", "--periods", "11")
if code == 0 {
t.Error("expected non-zero exit code for --periods 11")
}
expectErrorCode(t, stderr, "VALIDATION_ERROR")
}

func TestCompanyFinancials_InvalidStatement(t *testing.T) {
_, stderr, code := executeCommand("company", "financials", "E02144", "--statement", "invalid")
if code == 0 {
t.Error("expected non-zero exit code for invalid --statement")
}
expectErrorCode(t, stderr, "VALIDATION_ERROR")
}

func TestCompanyFinancials_HelpOutput(t *testing.T) {
stdout, _, code := executeCommand("company", "financials", "--help")
if code != 0 {
t.Error("company financials --help should succeed")
}
if stdout == "" {
t.Error("expected help output")
}
}
41 changes: 41 additions & 0 deletions cmd/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ var (

docTextSection string
docTextListSections bool

docFinancialStatement string
docFinancialNonConsolidated bool
)

var downloadTypeMap = map[string]int{
Expand Down Expand Up @@ -176,6 +179,40 @@ var docDataCmd = &cobra.Command{
},
}

var docFinancialCmd = &cobra.Command{
Use: "financial <docID>",
Short: "Extract structured financial statements from CSV",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
docID := args[0]
if !isValidDocID(docID) {
return &api.EDINETError{Code: api.ErrValidation, Message: fmt.Sprintf("invalid docID %q: must match S followed by digits (e.g. S100ABCD)", docID)}
}
if err := validateStatement(docFinancialStatement); err != nil {
return err
}
if app.Config.SubscriptionKey == "" {
return &api.EDINETError{Code: api.ErrAuth, Message: "EDINET_API_KEY environment variable is required"}
}

client := api.NewClient(app.Config.SubscriptionKey, "https://api.edinet-fsa.go.jp", app.Config.Debug)
svc := service.NewFinancialService(client, app.Cache)

opts := service.StatementOpts{
Statement: docFinancialStatement,
}
if docFinancialNonConsolidated {
opts.Consolidated = ptrBool(false)
}

result, err := svc.GetStatements(cmd.Context(), docID, opts)
if err != nil {
return err
}
return outputResult(cmd.OutOrStdout(), result)
},
}

var docTextCmd = &cobra.Command{
Use: "text [docID]",
Short: "Extract text from document HTML (best-effort)",
Expand Down Expand Up @@ -311,9 +348,13 @@ func init() {
docTextCmd.Flags().StringVar(&docTextSection, "section", "", "Section ID or heading pattern")
docTextCmd.Flags().BoolVar(&docTextListSections, "list-sections", false, "List available sections")

docFinancialCmd.Flags().StringVar(&docFinancialStatement, "statement", "all", "Statement type: bs, pl, cf, all")
docFinancialCmd.Flags().BoolVar(&docFinancialNonConsolidated, "non-consolidated", false, "Prefer non-consolidated statements")

docCmd.AddCommand(docListCmd)
docCmd.AddCommand(docGetCmd)
docCmd.AddCommand(docDataCmd)
docCmd.AddCommand(docTextCmd)
docCmd.AddCommand(docFinancialCmd)
rootCmd.AddCommand(docCmd)
}
30 changes: 30 additions & 0 deletions cmd/doc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ func executeCommand(args ...string) (stdout, stderr string, exitCode int) {
return outBuf.String(), errBuf.String(), exitCode
}

// expectErrorCode asserts the stderr JSON contains the expected error code.
func expectErrorCode(t *testing.T, stderr, wantCode string) {
t.Helper()
var errResp struct {
Error struct {
Code string `json:"code"`
} `json:"error"`
}
if err := json.Unmarshal([]byte(stderr), &errResp); err == nil {
if errResp.Error.Code != wantCode {
t.Errorf("error code = %q, want %q", errResp.Error.Code, wantCode)
}
}
}

func TestDocList_NoDateFlag(t *testing.T) {
_, stderr, code := executeCommand("doc", "list")
if code == 0 {
Expand Down Expand Up @@ -101,6 +116,21 @@ func TestDocText_NoDocID(t *testing.T) {
}
}

func TestDocFinancial_NoDocID(t *testing.T) {
_, _, code := executeCommand("doc", "financial")
if code == 0 {
t.Error("expected non-zero exit code when no docID provided")
}
}

func TestDocFinancial_InvalidStatement(t *testing.T) {
_, stderr, code := executeCommand("doc", "financial", "S100ABCD", "--statement", "invalid")
if code == 0 {
t.Error("expected non-zero exit code for invalid --statement")
}
expectErrorCode(t, stderr, "VALIDATION_ERROR")
}

func TestDocText_ListSectionsOutput(t *testing.T) {
stdout, _, code := executeCommand("doc", "text", "--list-sections")
if code != 0 {
Expand Down
16 changes: 16 additions & 0 deletions cmd/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,22 @@ func renderMapTable(w io.Writer, rows []map[string]any) error {
return nil
}

// validStatements defines accepted --statement flag values.
var validStatements = map[string]bool{
"bs": true, "pl": true, "cf": true, "all": true,
}

// validateStatement checks that stmt is a valid statement type.
func validateStatement(stmt string) error {
if !validStatements[stmt] {
return &api.EDINETError{Code: api.ErrValidation, Message: fmt.Sprintf("invalid --statement %q, must be one of: bs, pl, cf, all", stmt)}
}
return nil
}

// ptrBool returns a pointer to b. Used for optional *bool flags.
func ptrBool(b bool) *bool { return &b }

// exitError writes a structured error to w and returns the appropriate exit code.
func exitError(w io.Writer, err error) int {
if edinetErr, ok := err.(*api.EDINETError); ok {
Expand Down
10 changes: 10 additions & 0 deletions cmd/schema.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"github.com/beatinaniwa/edinet-cli/internal/financial"
"github.com/beatinaniwa/edinet-cli/internal/schema"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -34,9 +35,18 @@ var schemaSectionsCmd = &cobra.Command{
},
}

var schemaFinancialElementsCmd = &cobra.Command{
Use: "financial-elements",
Short: "List all known financial XBRL element mappings",
RunE: func(cmd *cobra.Command, args []string) error {
return outputResult(cmd.OutOrStdout(), financial.Elements())
},
}

func init() {
schemaCmd.AddCommand(schemaCommandsCmd)
schemaCmd.AddCommand(schemaDocTypesCmd)
schemaCmd.AddCommand(schemaSectionsCmd)
schemaCmd.AddCommand(schemaFinancialElementsCmd)
rootCmd.AddCommand(schemaCmd)
}
54 changes: 54 additions & 0 deletions cmd/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,60 @@ func TestSchemaSections_OutputIsJSON(t *testing.T) {
}
}

func TestSchemaFinancialElements_OutputIsJSON(t *testing.T) {
stdout, _, code := executeCommand("schema", "financial-elements")
if code != 0 {
t.Fatalf("schema financial-elements exit code = %d, want 0", code)
}
if !json.Valid([]byte(stdout)) {
t.Errorf("output is not valid JSON: %q", stdout[:min(100, len(stdout))])
}
// Verify it's an array
var elems []map[string]any
if err := json.Unmarshal([]byte(stdout), &elems); err != nil {
t.Fatalf("failed to parse output as array: %v", err)
}
if len(elems) == 0 {
t.Error("expected non-empty elements array")
}
}

func TestSchemaCommands_ContainsDocFinancial(t *testing.T) {
stdout, _, _ := executeCommand("schema", "commands")
var cmds []map[string]any
if err := json.Unmarshal([]byte(stdout), &cmds); err != nil {
t.Fatalf("failed to parse output: %v", err)
}
found := false
for _, c := range cmds {
if c["name"] == "doc financial" {
found = true
break
}
}
if !found {
t.Error("schema commands output missing 'doc financial'")
}
}

func TestSchemaCommands_ContainsSchemaFinancialElements(t *testing.T) {
stdout, _, _ := executeCommand("schema", "commands")
var cmds []map[string]any
if err := json.Unmarshal([]byte(stdout), &cmds); err != nil {
t.Fatalf("failed to parse output: %v", err)
}
found := false
for _, c := range cmds {
if c["name"] == "schema financial-elements" {
found = true
break
}
}
if !found {
t.Error("schema commands output missing 'schema financial-elements'")
}
}

func TestSchemaCommands_ContainsDocList(t *testing.T) {
stdout, _, _ := executeCommand("schema", "commands")
var cmds []map[string]any
Expand Down
Loading
Loading