Friday, May 30, 2025

Customizing the CT600

Now that we are generating the CT600, we have the ability to customize it. Most obviously, we need to put in our credentials, but there will be other things too as we move along. I think there will ultimately be four sources of data:
  • Things that are at the "app level" and would be the same across all clients, such as the credentials I received from SDST;
  • Things that are specific to the fact that we are submitting a CT600 (the overall gateway supports a lot more things, most obviously SA100s);
  • Things that are specific to this customer (i.e. my company) such as the UTR, but do not change year on year;
  • The actual accounts and tax details themselves.
To avoid falling into the trap that the ct600 program fell into, I am going to try and keep these "as close to where they belong" as I can. I think I need a special (second) config file to pull in my app secrets; this will allow anyone else who wants to use this code (idiots!) to sign up with SDST and fill in that configuration.

All the information related to it being a CT600 can either go in my existing configuraiton file or even a more general one, depending on how specific it is to the business.

I think I want to put things like the UTR into the spreadsheet, in a special "options" sheet.

OK, that's what I think, but I'm happy to be wrong. But what I need is to have these things available in the govtalk.go code that we wrote last time. So let's start there (where everything can be assumed to have been consolidated) and work our way backwards.

We can update our test to generate the file but not bother submitting it (especially since I'm on a train right now with very spotty wifi) but to create a Config and pass that in.
func TestWeCanConfigureSomething(t *testing.T) {
    config := &config.Config{}
    config.Sender = "me"
    config.Password = "secret"
    config.Utr = "1234509876"
    config.Vendor = "Ziniki"
    config.Product = "SendTax"
    config.Version = "2025-05-30-Alpha"
    send, err := submission.Generate(config)

CT600_CONFIG_1:accounts/internal/ct600/submission/submission_test.go

Here's the Config struct, at least so far as we've gone so far:
package config

type Config struct {
    Sender, Password         string
    Utr                      string
    Vendor, Product, Version string
}

CT600_CONFIG_1:accounts/internal/ct600/config/config.go

And in the generation, we can use these configured parameters to customize the "real world" GovTalk object:
func Generate(conf *config.Config) (io.Reader, error) {
    msg := govtalk.MakeGovTalk()
    msg.Identity(conf.Sender, conf.Password)
    msg.Utr(conf.Utr)
    msg.Product(conf.Vendor, conf.Product, conf.Version)
    bs, err := xml.MarshalIndent(msg.AsXML(), "", "  ")
    if err != nil {
        return nil, err
    }
    // fmt.Printf("%s", string(bs))
    return bytes.NewReader(bs), nil
}

CT600_CONFIG_1:accounts/internal/ct600/submission/generate.go

And, in turn, that can then generate the appropriate XML for those parameters:
type GovTalkMessage struct {
    sender, password         string
    utr                      string
    vendor, product, version string
}

func (gtm *GovTalkMessage) Identity(send, pwd string) {
    gtm.sender = send
    gtm.password = pwd
}

func (gtm *GovTalkMessage) Utr(utr string) {
    gtm.utr = utr
}

func (gtm *GovTalkMessage) Product(vendor, product, version string) {
    gtm.vendor = vendor
    gtm.product = product
    gtm.version = version
}

func (gtm *GovTalkMessage) AsXML() any {
    env := ElementWithText("EnvelopeVersion", "2.0")
    msgDetails := ElementWithNesting(
        "MessageDetails",
        ElementWithText("Class", "HMRC-CT-CT600"),
        ElementWithText("Qualifier", "request"),
        ElementWithText("Function", "submit"),
        ElementWithText("Transformation", "XML"),
        ElementWithText("GatewayTest", "1"),
    )
    sndrDetails := ElementWithNesting(
        "SenderDetails",
        ElementWithNesting(
            "IDAuthentication",
            ElementWithText("SenderID", gtm.sender),
            ElementWithNesting(
                "Authentication",
                ElementWithText("Method", "clear"),
                ElementWithText("Role", "Principal"),
                ElementWithText("Value", gtm.password),
            ),
        ),
    )
    gtDetails := ElementWithNesting(
        "GovTalkDetails",
        ElementWithNesting("Keys", Key("UTR", gtm.utr)),
        ElementWithNesting("TargetDetails", ElementWithText("Organisation", "HMRC")),
        ElementWithNesting(
            "ChannelRouting",
            ElementWithNesting(
                "Channel",
                ElementWithText("URI", gtm.vendor),
                ElementWithText("Product", gtm.product),
                ElementWithText("Version", gtm.version),
            ),
        ),
    )
    return MakeGovTalkMessage(env,
        ElementWithNesting("Header", msgDetails, sndrDetails),
        gtDetails,
        gtm.makeBody())
}

func (gtm *GovTalkMessage) makeBody() any {
    body := ElementWithNesting("Body")
    return &body
}

CT600_CONFIG_1:accounts/internal/ct600/govtalk/govtalk.go

Reading the configuration

So let's say that we don't want to set up a configuration in code but instead want to read it from files on the file system. How could we do that?

Well, we already have a lot of that code which reads the configuration to map all our tables, so we can copy, reuse or generalize that to read some other config files and then do that here.

Looking at that reader code, the big problem with it is that it creates its own Configuration object internally, which stops us passing our own in. However, we could extract that and allow the reader portion to take an object (which must at least implement Configuration so that we can extract the verbs). Any additional configuration will need to happen in the wrapper.

Let's update our test:
func TestWeCanConfigureSomething(t *testing.T) {
    config := &config.Config{}
    err := gcconf.ReadAConfiguration(config, "../../../testdata/foo.json")
    if err != nil {
        t.Fatalf("%v", err)
    }
    send, err := submission.Generate(config)

CT600_CONFIG_2:accounts/internal/ct600/submission/submission_test.go

The foo.json file is something I just created with dummy data and put in testdata at the top level. All those .. elements just say "go up to the top level".
{
    "Sender": "a-user",
    "Password": "secret",
    "Utr": "1234509876",
    "Vendor": "Ziniki",
    "Product": "SendTax",
    "Version": "2025-05-30-Alpha"
}

CT600_CONFIG_2:accounts/testdata/foo.json

I decided to refactor the existing configuration reader as discussed above. I ended up with this:
func ReadConfig(file string) (*Configuration, error) {
    ret := Configuration{VerbMap: make(map[string]*Verb)}
    err := ReadAConfiguration(&ret, file)
    if err != nil {
        return nil, err
    } else {
        return &ret, nil
    }
}

func ReadAConfiguration(config any, file string) error {
    bs, err := os.ReadFile(file)
    if err != nil {
        return err
    }
    err = json.Unmarshal(bs, config)
    if err != nil {
        panic(err)
    }
    vc, isConfig := config.(Configuration)
    if isConfig {
        for _, v := range vc.Verbs {
            vc.VerbMap[v.Name] = &v
        }
    }
    return nil
}

CT600_CONFIG_2:accounts/internal/gnucash/config/reader.go

I had intended that the config argument of ReadAConfiguration would be of type Configuration, but it would seem that's just an expectation I have coming from a Java background. But I am able to use any successfully, so I'm fine with that. A caveat, of course, is that the code to initialize the verbs depends on the instance actually being a Configuration, but we can easily test that and only execute that code if it's true.

One of the great things about "key/value" configuration is that we can say "well, there are four configuration files, but I don't really care, just use all four to initialize this object and then check it's done afterwards". It doesn't matter which parameters come from which files. We should be able to call ReadAConfiguration multiple times. Of course, I'm not sure that the code to iterate over the verbs will work perfectly every time, but we can sort that out if it is a problem.

Finally, of course, we need to update our Config to "include" a GnuCash Configuration, and we need to provide an equivalent "create-and-read" function, although in the fulness of time I think we will want this to accept multiple configuration files.
type Config struct {
    config.Configuration
    Sender, Password         string
    Utr                      string
    Vendor, Product, Version string
}

func ReadConfig(file string) (*Config, error) {
    ret := Config{Configuration: config.Configuration{VerbMap: make(map[string]*config.Verb)}}
    err := config.ReadAConfiguration(&ret, file)
    if err != nil {
        return nil, err
    } else {
        return &ret, nil
    }
}

CT600_CONFIG_2:accounts/internal/ct600/config/config.go

The nested configuration initialization seems kind of klunky, so I imagine at some point I will refactor this. I may or may not mention it when I'm doing it.

Conclusion

That feels enough for today. We've customized what we're doing and we've enabled that to be read from a file. I think my next step will be to put my actual parameters in actual files, create a cmd file, and delete this non-test.

I will also want to pull some of the configuration parameters from the spreadsheet as we read that. Oh, what fun!

No comments:

Post a Comment