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/c14nIt contains on the web page "general usage instructions", so I'm just going to copy and paste that.
go: downloading github.com/ucarion/c14n v0.1.0
go: added github.com/ucarion/c14n v0.1.0
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) {Well, I think I've done everything correctly, but sadly, it did not work:
// 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
}
---- xmllint outputNow, 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.
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.
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