Friday, July 11, 2025

CompanyTaxReturn


So the main element that we want to submit in our Body is a CompanyTaxReturn element. However, the schema requires that this is wrapped up in an IRenvelope, to specify things like the UTR. I have the government's sample, and the next thing I'm going to do is to try and build up code to generate something that is as similar to that as I can. Here's the Body of the sample:
    <Body>
        <IRenvelope xmlns="http://www.govtalk.gov.uk/taxation/CT/5">
            <IRheader>
                <Keys>
                    <Key Type="UTR">8596148860</Key>
                </Keys>
                <PeriodEnd>2022-03-31</PeriodEnd>
                <DefaultCurrency>GBP</DefaultCurrency>
                <Manifest>
                    <Contains>
                        <Reference>
                            <Namespace>http://www.govtalk.gov.uk/taxation/CT/5</Namespace>
                            <SchemaVersion>2022-v1.99</SchemaVersion>
                            <TopElementName>CompanyTaxReturn</TopElementName>
                        </Reference>
                    </Contains>
                </Manifest>
                <IRmark Type="generic">Enter IRmark here</IRmark>
                <Sender>Company</Sender>
            </IRheader>
            <CompanyTaxReturn ReturnType="new">
                <CompanyInformation>
                    <CompanyName>Enter Company Name</CompanyName>
                    <RegistrationNumber>12345678</RegistrationNumber>
                    <!-- Will need to match the CH number in the accounts -->
                    <Reference>8596148860</Reference>
                    <!-- Reference will need to match the UTR used in the keys and the reference in the computations -->
                    <CompanyType>6</CompanyType>
                    <PeriodCovered>
                        <From>2021-04-01</From>
                        <To>2022-03-31</To>
                        <!-- Period covered To date will need to match the End period date in accounts -->
                    </PeriodCovered>
                </CompanyInformation>
                <ReturnInfoSummary>
                    <Accounts>
                        <NoAccountsReason>Not within charge to CT</NoAccountsReason>
                    </Accounts>
                    <Computations>
                        <NoComputationsReason>Not within charge to CT</NoComputationsReason>
                    </Computations>
                </ReturnInfoSummary>
                <Turnover>
                    <Total>100000.00</Total>
                </Turnover>
                <CompanyTaxCalculation>
                    <Income>
                        <Trading>
                            <Profits>100000.00</Profits>
                            <LossesBroughtForward>0.00</LossesBroughtForward>
                            <NetProfits>100000.00</NetProfits>
                        </Trading>
                    </Income>
                    <ProfitsBeforeOtherDeductions>100000.00</ProfitsBeforeOtherDeductions>
                    <ChargesAndReliefs>
                        <ProfitsBeforeDonationsAndGroupRelief>100000.00</ProfitsBeforeDonationsAndGroupRelief>
                    </ChargesAndReliefs>
                    <ChargeableProfits>100000.00</ChargeableProfits>
                    <CorporationTaxChargeable>
                        <FinancialYearOne>
                            <Year>2021</Year>
                            <Details>
                                <Profit>100000.00</Profit>
                                <TaxRate>19.00</TaxRate>
                                <Tax>19000.00</Tax>
                            </Details>
                        </FinancialYearOne>
                    </CorporationTaxChargeable>
                    <CorporationTax>19000.00</CorporationTax>
                    <NetCorporationTaxChargeable>19000.00</NetCorporationTaxChargeable>
                    <TaxReliefsAndDeductions>
                        <TotalReliefsAndDeductions>0.00</TotalReliefsAndDeductions>
                    </TaxReliefsAndDeductions>
                </CompanyTaxCalculation>
                <CalculationOfTaxOutstandingOrOverpaid>
                    <NetCorporationTaxLiability>19000.00</NetCorporationTaxLiability>
                    <TaxChargeable>19000.00</TaxChargeable>
                    <TaxPayable>19000.00</TaxPayable>
                </CalculationOfTaxOutstandingOrOverpaid>
                <Declaration>
                    <AcceptDeclaration>yes</AcceptDeclaration>
                    <Name>Test</Name>
                    <Status>Test</Status>
                </Declaration>
            </CompanyTaxReturn>
        </IRenvelope>
    </Body>

CT600_POLL_AFTER_SUBMIT:accounts/ct600/no-attach.xml

Once again, there's quite a lot to do there, so I'm just going to wander off for a bit and do all this. It's mainly just more mucking about with XML.

OK, a couple of hours later...

The first step in the process is that there are more options in the submitOptions in submit.go:
func Submit(conf *config.Config) error {
    utr := conf.Utr
    if utr == "" {
        utr = conf.Business.TaxNum
    }
    ctr := &govtalk.IRenvelope{Business: conf.Business, ReturnType: "new",
        Sender: "Company", // the type of business we are, I believe.  The schema limits it to a handful of options

        UTR:         utr,
        PeriodStart: "2021-04-01", PeriodEnd: "2022-03-31",
        Turnover: 100000.0, TradingProfits: 0, LossesBroughtForward: 0, TradingNetProfits: 0,
        CorporationTax: 0,
    }
    submitOptions := &govtalk.EnvelopeOptions{Qualifier: "request", Function: "submit", IncludeSender: true, IncludeKeys: true, IncludeBody: true, IRenvelope: ctr}
    send, err := Generate(conf, submitOptions)

CT600_WITH_BODY:accounts/internal/ct600/submission/submit.go

You will immediately notice that a lot of these numbers look dubious. That's because I'm not at the point of trying to actually get correct accounts in. I just want some numbers in. So I'm largely just copying entries from the sample above. Ultimately, obviously, they will need to be calculated from the spreadsheets we already have. We'll get to that.

The immediate knock-on effects are seen in makeBody in GovTalkMessage:
func (gtm *GovTalkMessage) makeBody() *SimpleElement {
    body := ElementWithNesting("Body")
    body.Elements = append(body.Elements, gtm.opts.IRenvelope.AsXML())
    return body
}

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

which calls the AsXML method on the IRenvelope we created and passed in. There's a lot of that, so here are the "highlights":
type IRenvelope struct {
    Business   config.Business
    UTR        string
    ReturnType string

    PeriodStart string
    PeriodEnd   string
    Sender      string

    Turnover             float64
    TradingProfits       float64
    LossesBroughtForward float64
    TradingNetProfits    float64
    CorporationTax       float64
}

func (ire *IRenvelope) AsXML() any {
    keys := ElementWithNesting("Keys", Key("UTR", ire.UTR))
    pe := ElementWithText("PeriodEnd", ire.PeriodEnd)
    dc := ElementWithText("DefaultCurrency", "GBP")
    manifest := ElementWithNesting("Manifest",
        ElementWithNesting("Contains",
            ElementWithNesting("Reference",
                ElementWithText("Namespace", "http://www.govtalk.gov.uk/taxation/CT/5"),
                ElementWithText("SchemaVersion", "2022-v1.99"),
                ElementWithText("TopElementName", "CompanyTaxReturn"),
            )))
    irh := ElementWithNesting("IRheader", keys, pe, dc, manifest)
    irh.Elements = append(irh.Elements, ElementWithText("Sender", ire.Sender))
    ci := ElementWithNesting("CompanyInformation", companyInfo(ire.Business, ire.UTR, ire.PeriodStart, ire.PeriodEnd))
    summary := ElementWithNesting("ReturnInfoSummary", accounts(), computations())
    turnover := ElementWithNesting("Turnover", ElementWithText("Total", fmt.Sprintf("%.2f", ire.Turnover)))
    calc := ElementWithNesting("CompanyTaxCalculation", ire.taxCalc())
    too := ElementWithNesting("CalculationOfTaxOutstandingOrOverpaid", ire.cotoo())
    decl := ElementWithNesting("Declaration", decl())
    ctr := MakeCompanyTaxReturn(ire.ReturnType, ci, summary, turnover, calc, too, decl)
    return MakeIRenvelopeMessage(irh, ctr)
}

CT600_WITH_BODY:accounts/internal/ct600/govtalk/irenvelope.go

And, with a lot of application and effort, we get very close to something that works.

The IRmark Saga

It was all going swimmingly until I had to deal with the IRmark field shown in the sample with the helpful comment:
               <IRmark Type="generic">Enter IRmark here</IRmark>
Now, it would seem that the IRmark is some kind of non-repudiation mechanism, but at this point I don't quite understand it, or why I need to calculate this hash, since they say they are going to calculate the same value. It seems they could calculate the hash and sign it and it would be just as effective.

Anyway, in order to calculate this we have to "canonicalise the XML without the IRmark, then do a SHA-1, then convert to Base64, then insert it into the document". I'm not going to do all that right now, so I'm going to harcode an IRmark and do the (not insignificant) work in inserting it into the right spot:

In the AsXML method of GovTalkMessage:
    var body *SimpleElement
    if gtm.opts.IncludeBody {
        body = gtm.makeBody()
        attachIRmark(body, calculateIRmark(body))
    }

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

And then we can add the two methods referenced here:
func attachIRmark(body *SimpleElement, irmark string) {
    log.Printf("%p", body)
    ire := body.Elements[0].(*IRenvelopeXML)
    irh := ire.Elements[0].(*SimpleElement)
    irh.Elements = slices.Insert(irh.Elements, len(irh.Elements)-1, any(MakeIRmark(irmark)))
}

func calculateIRmark(body *SimpleElement) string {
    return "need to calculate a SHA-1 tag"
}

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

We can now run through and submit our current tax return and, unsurprisingly, find that the government did not come up with the same IRmark:
---- xmllint output
submit.xml validates
----
2 error(s) reported:
  Code Raised By Location Type Message
  3001 Department business The submission of this document has failed due to departmental specific business logic in the Body tag.
  2021 ChRIS IRmark business The supplied IRmark is incorrect.

Generating the IRmark

So, let's go back and do the job properly.

Starting off with canonicalisation ("c14n" apparently, but I'm not going to count), there is a Go implementation called ucarion/c14n, so let's start by installing that:
$ go get github.com/ucarion/c14n
go: downloading github.com/ucarion/c14n v0.1.0
go: added github.com/ucarion/c14n v0.1.0
It contains on the web page "general usage instructions", so I'm just going to copy and paste that.
func calculateIRmark(body *SimpleElement) (string, error) {
    bs, err := xml.MarshalIndent(body, "", "  ")
    if err != nil {
        return "", err
    }
    decoder := xml.NewDecoder(bytes.NewReader(bs))
    out, err := c14n.Canonicalize(decoder)
    fmt.Println(string(out), err)
    return "hello, world", nil
}

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

Carrying on from there, the next step is to deduce the SHA-1 hash of the "body" of the message. It's not clear what is meant by that, but after looking around the internet, I settled on encoding the whole message.

And then we need to encode that in "base64". One of the problems of using Go is that it has more options than most other languages, including a whole bunch of base64 encodings. After a while I decided that StdEncoding was the most likely to get me where I wanted to go.

Getting all this to work involved a little bit of reordering of the calling method as well, in order to have the whole message before we attach the IRmark.
    var body *SimpleElement
    if gtm.opts.IncludeBody {
        body = gtm.makeBody()
    }

    gt := MakeGovTalkMessage(env,
        ElementWithNesting("Header", msgDetails, sndrDetails),
        gtDetails,
        body)

    if body != nil {
        irmark, err := calculateIRmark(gt)
        if err != nil {
            return nil, err
        }
        attachIRmark(body, irmark)
    }

    return gt, nil

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

And then we have the additional steps.
func calculateIRmark(body any) (string, error) {
    // Generate a text representation
    bs, err := xml.MarshalIndent(body, "", "  ")
    if err != nil {
        return "", err
    }

    // now canonicalise that
    decoder := xml.NewDecoder(bytes.NewReader(bs))
    out, err := c14n.Canonicalize(decoder)
    if err != nil {
        return "", err
    }

    // Generate a SHA-1 encoding
    hasher := sha1.New()
    _, err = hasher.Write(out)
    if err != nil {
        return "", err
    }
    sha := hasher.Sum(nil)

    // And then turn that into Base64
    w := new(bytes.Buffer)
    enc := base64.NewEncoder(base64.StdEncoding, w)
    enc.Write(sha)
    enc.Close()

    // The string of this is the IRmark
    b64sha := w.String()
    fmt.Printf("IRmark: %d %s\n", len(b64sha), b64sha)

    return b64sha, nil
}
Well, I think I've done everything correctly, but sadly, it did not work:
---- xmllint output
submit.xml validates
----
2 error(s) reported:
  Code Raised By Location Type Message
  3001 Department business The submission of this document has failed due to departmental specific business logic in the Body tag.
  2021 ChRIS IRmark business The supplied IRmark is incorrect.
Now, I know that what this means: they have calculated an IRmark and come up with a different value. As usual with security measures, I'm frustrated because they only tell me that my value is "wrong" and don't offer an alternative value that is "correct". That leaves me fumbling in the dark. I understand that security is like this (you don't want to give people help guessing passwords, for example), but I really don't see what the risk would be here.

Anyway, as I do when I'm stuck, I go on a Google Hunt. I found this project for which I have no idea of the provenance, but they are clearly advertising something that does what I want to do. The implication would be that it works. I copied it, downloaded the library and got it all working in Eclipse. I copied the XML it creates to my Go instance and ran calculateIRmark on it: same value. I took the XML I was generating (without the IRmark node) and ran it through their code: same value.

OK, so I've proved I'm "bug-compatible" with another project on the internet.

So once again, I gave up and emailed the SDSTeam everything I had. Hopefully they will get back to me soon.

Conclusion

I've made some progress working my way through building up a CompanyTaxReturn document. I think if it weren't for the IRmark issue, I would now have a successful submission and I could turn to the "final" step of doing the correct tax calculations for my business and including them.

Sadly, that will have to wait until I can get the IRmark issue sorted.

No comments:

Post a Comment