├── .gitattributes
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── LICENSE
├── QuickBooksSharp.CodeGen
├── CodeModel
│ ├── ClassModel.cs
│ ├── EnumModel.cs
│ └── PropertyModel.cs
├── Program.cs
├── QuickBooksSharp.CodeGen.csproj
└── xsd
│ ├── 3.56
│ ├── Finance.xsd
│ ├── IntuitBaseTypes.xsd
│ ├── IntuitNamesTypes.xsd
│ ├── IntuitRestServiceDef.xsd
│ ├── Report.xsd
│ └── SalesTax.xsd
│ ├── 3.65
│ ├── Finance.xsd
│ ├── IntuitBaseTypes.xsd
│ ├── IntuitNamesTypes.xsd
│ ├── IntuitRestServiceDef.xsd
│ ├── Report.xsd
│ └── SalesTax.xsd
│ ├── 3.73
│ ├── Finance.xsd
│ ├── IntuitBaseTypes.xsd
│ ├── IntuitNamesTypes.xsd
│ ├── IntuitRestServiceDef.xsd
│ ├── Report.xsd
│ └── SalesTax.xsd
│ └── 3.75
│ ├── Finance.xsd
│ ├── IntuitBaseTypes.xsd
│ ├── IntuitNamesTypes.xsd
│ ├── IntuitRestServiceDef.xsd
│ ├── Report.xsd
│ └── SalesTax.xsd
├── QuickBooksSharp.Tests
├── AuthenticationService_Tests.cs
├── DataServiceTests.cs
├── QuickBooksSharp.Tests.csproj
├── QuickBooksUrlTests.cs
├── RunPolicyTests.cs
├── ServiceTestBase.cs
├── TestHelper.cs
└── WebhookEventTests.cs
├── QuickBooksSharp.sln
├── QuickBooksSharp
├── Authentication
│ ├── AuthenticationService.cs
│ ├── IAuthenticationService.cs
│ ├── RevokeTokenRequest.cs
│ ├── TokenRequest.cs
│ ├── TokenResponse.cs
│ └── UserInfo.cs
├── Entities
│ ├── Generated.cs
│ ├── IntuitResponseOfT.cs
│ ├── QueryCountResponse.cs
│ └── QueryResponseOfT.cs
├── Helper.cs
├── Infrastructure
│ ├── IQuickBooksHttpClient.cs
│ ├── NumberTimespanConverter.cs
│ ├── QuickBooksException.cs
│ ├── QuickBooksHttpClient.cs
│ └── QuickBooksUrl.cs
├── Policies
│ ├── FifoSemaphore.cs
│ ├── IRunPolicy.cs
│ ├── MaxConcurrencyRetryRunPolicy.cs
│ ├── NoRetryRunPolicy.cs
│ ├── QuickBooksAPIResponse.cs
│ ├── RateLimitEvent.cs
│ ├── RunPolicy.cs
│ └── SimpleRetryRunPolicy.cs
├── QuickBooksSharp.csproj
├── Services
│ ├── DataService.cs
│ └── IDataService.cs
└── Webhooks
│ ├── DataChangeEvent.cs
│ ├── EntityChange.cs
│ ├── EntityChangedName.cs
│ ├── EventNotification.cs
│ └── WebhookEvent.cs
└── README.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Build Test Publish
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches: [ master ]
7 | pull_request:
8 | branches: [ master ]
9 |
10 | jobs:
11 | build:
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v4
17 | - name: Setup .NET Core SDK
18 | uses: actions/setup-dotnet@v4
19 | with:
20 | dotnet-version: '9.0.x'
21 | - name: Install dependencies
22 | run: dotnet restore
23 | - name: Build
24 | run: dotnet build --configuration Release --no-restore
25 | - name: Test
26 | env:
27 | QUICKBOOKS_SHARP_CLIENT_ID: ${{ secrets.QUICKBOOKS_SHARP_CLIENT_ID }}
28 | QUICKBOOKS_SHARP_CLIENT_SECRET: ${{ secrets.QUICKBOOKS_SHARP_CLIENT_SECRET }}
29 | QUICKBOOKS_SHARP_OAUTH_REDIRECT_URI: ${{ secrets.QUICKBOOKS_SHARP_OAUTH_REDIRECT_URI }}
30 | QUICKBOOKS_SHARP_REFRESH_TOKEN: ${{ secrets.QUICKBOOKS_SHARP_REFRESH_TOKEN }}
31 | QUICKBOOKS_SHARP_REALMID: ${{ secrets.QUICKBOOKS_SHARP_REALMID }}
32 | run: dotnet test --no-restore --verbosity normal
33 | - name: Publish to nuget
34 | env:
35 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
36 | run: dotnet nuget push QuickBooksSharp/bin/Release/*.nupkg -s https://api.nuget.org/v3/index.json --skip-duplicate -k $NUGET_API_KEY
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Oo]ut/
33 | [Ll]og/
34 | [Ll]ogs/
35 |
36 | # Visual Studio 2015/2017 cache/options directory
37 | .vs/
38 | # Uncomment if you have tasks that create the project's static files in wwwroot
39 | #wwwroot/
40 |
41 | # Visual Studio 2017 auto generated files
42 | Generated\ Files/
43 |
44 | # MSTest test Results
45 | [Tt]est[Rr]esult*/
46 | [Bb]uild[Ll]og.*
47 |
48 | # NUnit
49 | *.VisualState.xml
50 | TestResult.xml
51 | nunit-*.xml
52 |
53 | # Build Results of an ATL Project
54 | [Dd]ebugPS/
55 | [Rr]eleasePS/
56 | dlldata.c
57 |
58 | # Benchmark Results
59 | BenchmarkDotNet.Artifacts/
60 |
61 | # .NET Core
62 | project.lock.json
63 | project.fragment.lock.json
64 | artifacts/
65 |
66 | # ASP.NET Scaffolding
67 | ScaffoldingReadMe.txt
68 |
69 | # StyleCop
70 | StyleCopReport.xml
71 |
72 | # Files built by Visual Studio
73 | *_i.c
74 | *_p.c
75 | *_h.h
76 | *.ilk
77 | *.meta
78 | *.obj
79 | *.iobj
80 | *.pch
81 | *.pdb
82 | *.ipdb
83 | *.pgc
84 | *.pgd
85 | *.rsp
86 | *.sbr
87 | *.tlb
88 | *.tli
89 | *.tlh
90 | *.tmp
91 | *.tmp_proj
92 | *_wpftmp.csproj
93 | *.log
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio LightSwitch build output
298 | **/*.HTMLClient/GeneratedArtifacts
299 | **/*.DesktopClient/GeneratedArtifacts
300 | **/*.DesktopClient/ModelManifest.xml
301 | **/*.Server/GeneratedArtifacts
302 | **/*.Server/ModelManifest.xml
303 | _Pvt_Extensions
304 |
305 | # Paket dependency manager
306 | .paket/paket.exe
307 | paket-files/
308 |
309 | # FAKE - F# Make
310 | .fake/
311 |
312 | # CodeRush personal settings
313 | .cr/personal
314 |
315 | # Python Tools for Visual Studio (PTVS)
316 | __pycache__/
317 | *.pyc
318 |
319 | # Cake - Uncomment if you are using it
320 | # tools/**
321 | # !tools/packages.config
322 |
323 | # Tabs Studio
324 | *.tss
325 |
326 | # Telerik's JustMock configuration file
327 | *.jmconfig
328 |
329 | # BizTalk build output
330 | *.btp.cs
331 | *.btm.cs
332 | *.odx.cs
333 | *.xsd.cs
334 |
335 | # OpenCover UI analysis results
336 | OpenCover/
337 |
338 | # Azure Stream Analytics local run output
339 | ASALocalRun/
340 |
341 | # MSBuild Binary and Structured Log
342 | *.binlog
343 |
344 | # NVidia Nsight GPU debugger configuration file
345 | *.nvuser
346 |
347 | # MFractors (Xamarin productivity tool) working folder
348 | .mfractor/
349 |
350 | # Local History for Visual Studio
351 | .localhistory/
352 |
353 | # BeatPulse healthcheck temp database
354 | healthchecksdb
355 |
356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
357 | MigrationBackup/
358 |
359 | # Ionide (cross platform F# VS Code tools) working folder
360 | .ionide/
361 |
362 | # Fody - auto-generated XML schema
363 | FodyWeavers.xsd
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 better-reports
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/QuickBooksSharp.CodeGen/CodeModel/ClassModel.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace QuickBooksSharp.CodeGen
4 | {
5 | public class ClassModel
6 | {
7 | public string Name { get; set; }
8 |
9 | public string BaseName { get; set; }
10 |
11 | public bool IsAbstract { get; set; }
12 |
13 | public PropertyModel[] Properties { get; set; }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/QuickBooksSharp.CodeGen/CodeModel/EnumModel.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace QuickBooksSharp.CodeGen
4 | {
5 | public class EnumModel
6 | {
7 | public string Name { get; set; }
8 |
9 | public string[] Fields { get; set; }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/QuickBooksSharp.CodeGen/CodeModel/PropertyModel.cs:
--------------------------------------------------------------------------------
1 | namespace QuickBooksSharp.CodeGen
2 | {
3 | public class PropertyModel
4 | {
5 | public string Name { get; set; }
6 |
7 | public string TypeName { get; set; }
8 |
9 | public bool IsNullable { get; set; }
10 |
11 | public bool IsArray { get; set; }
12 |
13 | public string Code { get; set; }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/QuickBooksSharp.CodeGen/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CSharp;
3 | using Microsoft.CodeAnalysis.Formatting;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.IO;
7 | using System.Linq;
8 | using System.Text.RegularExpressions;
9 | using System.Xml.Schema;
10 |
11 | namespace QuickBooksSharp.CodeGen
12 | {
13 | class Program
14 | {
15 | ///
16 | /// -Download latest minor version XSD from https://developer.intuit.com/app/developer/qbo/docs/develop/explore-the-quickbooks-online-api/minor-versions
17 | /// -Unzip into the xsd/3.{MinorVersion} folder
18 | /// -Update QuickBooksUrl.cs MinorVersion
19 | /// -Update README.md
20 | /// -Run program
21 | ///
22 | ///
23 | static void Main(string[] args)
24 | {
25 | string currentDir = Directory.GetCurrentDirectory();
26 | string solutionPath = currentDir.Substring(0, currentDir.IndexOf("QuickBooksSharp"));
27 | string xsdPath = Path.Combine(solutionPath, $"QuickBooksSharp/QuickBooksSharp.CodeGen/xsd/{QuickBooksUrl.Version}");
28 | string outFilePath = Path.Combine(solutionPath, "QuickBooksSharp/QuickBooksSharp/Entities/Generated.cs");
29 | var schemas = Directory.GetFiles(xsdPath)
30 | .Select(filePath => XmlSchema.Read(new StringReader(File.ReadAllText(filePath)), null))
31 | .ToArray();
32 | var set = new XmlSchemaSet();
33 | foreach (var s in schemas)
34 | set.Add(s);
35 | set.Compile();
36 |
37 | var globalTypes = set.GlobalTypes.Values.Cast()
38 | .ToArray();
39 | var elts = set.GlobalElements.Values.Cast().ToArray();
40 |
41 | var eltNameToSubstitutedElements = elts.Where(e => !string.IsNullOrEmpty(e.SubstitutionGroup.Name))
42 | .ToLookup(e => e.SubstitutionGroup.Name);
43 | var simpleTypes = globalTypes.OfType().ToArray();
44 | var complexTypes = globalTypes.OfType().ToArray();
45 |
46 | var enums = GenerateEnums(simpleTypes);
47 | var classes = GenerateClasses(complexTypes, eltNameToSubstitutedElements);
48 | GenerateOutFile(outFilePath, enums, classes);
49 | FormatOutFile(outFilePath);
50 | }
51 |
52 | private static XmlSchemaComplexType GetParentComplexType(XmlSchemaObject o)
53 | {
54 | var p = o.Parent;
55 | while (p != null && p is not XmlSchemaComplexType)
56 | p = p.Parent;
57 |
58 | return p as XmlSchemaComplexType;
59 | }
60 |
61 | private static IEnumerable GetParticlesRec(XmlSchemaComplexType complexType, XmlSchemaSequence seq)
62 | {
63 | foreach (var item in seq.Items)
64 | {
65 | var parentComplexType = GetParentComplexType(item);
66 | if (parentComplexType != null && parentComplexType != complexType)
67 | continue;
68 | switch (item)
69 | {
70 | case XmlSchemaChoice choice:
71 | yield return choice;
72 | break;
73 |
74 | case XmlSchemaElement elt:
75 | yield return elt;
76 | break;
77 |
78 | case XmlSchemaSequence innerSeq:
79 | foreach (var p in GetParticlesRec(complexType, innerSeq))
80 | yield return p;
81 | break;
82 |
83 | case XmlSchemaAny _:
84 | //IntuitAnyType => no pties
85 | break;
86 |
87 | default:
88 | throw new Exception("Unexpected particle");
89 | }
90 | }
91 | }
92 |
93 | private static string GetTypeName(string typeName)
94 | {
95 | return typeName switch
96 | {
97 | "id" => "string",
98 | "anyURI" => "string",
99 | "positiveInteger" => "uint",
100 | "boolean" => "bool",
101 | "date" => "DateOnly",
102 | "dateTime" => "DateTimeOffset",
103 | "anyType" => "object",
104 | _ => typeName
105 | };
106 | }
107 |
108 | private static PropertyModel GetPropertyFromElt(XmlSchemaElement elt, bool forceIsNullable, bool forceIsArray)
109 | {
110 | return new PropertyModel
111 | {
112 | Name = elt.QualifiedName.Name ?? throw new Exception(),
113 | TypeName = GetTypeName(elt.ElementSchemaType.QualifiedName.Name),
114 | IsNullable = elt.MinOccurs == 0 || forceIsNullable,
115 | IsArray = elt.MaxOccurs == decimal.MaxValue || forceIsArray
116 | };
117 | }
118 |
119 | private static PropertyModel GetPropertyFromAttribute(XmlSchemaAttribute attr)
120 | {
121 | return new PropertyModel
122 | {
123 | Name = attr.QualifiedName.Name ?? throw new Exception(),
124 | TypeName = GetTypeName(attr.AttributeSchemaType.QualifiedName.Name),
125 | IsNullable = attr.Use == XmlSchemaUse.Optional,
126 | IsArray = false,
127 | };
128 | }
129 |
130 | private static ClassModel[] GenerateClasses(XmlSchemaComplexType[] complexTypes, ILookup eltNameToSubstitutedElements)
131 | {
132 | return complexTypes.Select(t =>
133 | {
134 | var pties = new List();
135 |
136 | foreach (var attr in t.Attributes.Cast())
137 | pties.Add(GetPropertyFromAttribute(attr));
138 |
139 | if (t.ContentTypeParticle.GetType().Name != "EmptyParticle")
140 | {
141 | var particles = GetParticlesRec(t, (XmlSchemaSequence)t.ContentTypeParticle);
142 | var elts = particles.SelectMany(p => p switch
143 | {
144 | XmlSchemaChoice choice => choice.Items.Cast().SelectMany(o =>
145 | {
146 | if (o is XmlSchemaElement e)
147 | return new[] { (Elt: e, IsChoiceChild: true) };
148 | else if (o is XmlSchemaSequence s)
149 | return GetParticlesRec(t, s).Cast().Select(e => (Elt: e, IsChoiceChild: true));
150 | else
151 | throw new Exception();
152 | }),
153 | XmlSchemaElement elt => new[] { (Elt: elt, IsChoiceChild: false) },
154 | _ => throw new Exception()
155 | });
156 | foreach (var i in elts)
157 | {
158 | if (eltNameToSubstitutedElements.Contains(i.Elt.QualifiedName.Name))
159 | {
160 | bool isArray = i.Elt.MaxOccurs == decimal.MaxValue;
161 | bool isNullable = i.Elt.MinOccurs == 0;
162 | var subsitutionsPties = new List();
163 | foreach (var subElt in eltNameToSubstitutedElements[i.Elt.QualifiedName.Name]
164 | .Where(i => i.QualifiedName.Name != t.Name))
165 | {
166 | var pty = GetPropertyFromElt(subElt, true, isArray);
167 | pties.Add(pty);
168 | subsitutionsPties.Add(pty);
169 | }
170 | bool isAnyNullabe = isNullable || i.IsChoiceChild;
171 | pties.Add(new PropertyModel
172 | {
173 | Name = i.Elt.QualifiedName.Name + (isArray ? "s" : ""),
174 | TypeName = i.Elt.ElementSchemaType.QualifiedName.Name,
175 | IsArray = isArray,
176 | IsNullable = isAnyNullabe,
177 | Code = "{ get => " + string.Join(" ?? ", subsitutionsPties.Select((p, index) => (index == subsitutionsPties.Count - 1 ? $"({i.Elt.ElementSchemaType.QualifiedName.Name}{(isArray ? "[]" : "")}{(isAnyNullabe ? "?" : "")})" : "") + p.Name)) + (isAnyNullabe ? "" : "!") + "; }"
178 | });
179 | }
180 | else
181 | {
182 | pties.Add(GetPropertyFromElt(i.Elt, i.IsChoiceChild, false));
183 | }
184 | }
185 | }
186 | else
187 | {
188 | var contentModel = (XmlSchemaSimpleContent)t.ContentModel;
189 | var extension = (XmlSchemaSimpleContentExtension)contentModel.Content;
190 |
191 | pties.Add(new PropertyModel
192 | {
193 | Name = "value",
194 | TypeName = t.BaseXmlSchemaType.QualifiedName.Name == "id" ? "string" : t.BaseXmlSchemaType.QualifiedName.Name,
195 | IsArray = false,
196 | IsNullable = false,
197 | });
198 | foreach (var attr in extension.Attributes.Cast())
199 | pties.Add(GetPropertyFromAttribute(attr));
200 | }
201 |
202 | bool isReferenceType = t.Name == "ReferenceType";
203 | bool isCustomFieldDefinitionType = t.Name == "CustomFieldDefinition";
204 | return new ClassModel
205 | {
206 | Name = t.Name,
207 | //todo create a custom converter that can deseriaize to the correct CustomFieldDefinition dervived type
208 | //For now, we simply mark the base type as a concrete type, since none of the dervied properties are returned by the API anyway
209 | IsAbstract = isCustomFieldDefinitionType ? false : t.IsAbstract,
210 | BaseName = isReferenceType ? null : t.BaseXmlSchemaType?.Name,
211 | Properties = pties.ToArray()
212 |
213 | };
214 | }).Where(c => c.Name != null)
215 | .ToArray();
216 | }
217 |
218 | private static EnumModel[] GenerateEnums(XmlSchemaSimpleType[] simpleTypes)
219 | {
220 | return simpleTypes.Select(t => new EnumModel
221 | {
222 | Name = t.Name,
223 | Fields = (t.Content as XmlSchemaSimpleTypeRestriction)
224 | .Facets
225 | .Cast()
226 | .Select(f => f.Value)
227 | .Distinct()
228 | .ToArray()
229 | }).Where(e => e.Fields.Any())
230 | .ToArray();
231 | }
232 |
233 | private static void GenerateOutFile(string outFilePath, EnumModel[] enums, ClassModel[] classes)
234 | {
235 | using (var writer = new StreamWriter(outFilePath))
236 | {
237 | writer.WriteLine("using System;");
238 | writer.WriteLine("using System.Runtime.Serialization;");
239 | writer.WriteLine("using System.Text.Json.Serialization;");
240 | writer.WriteLine();
241 | writer.WriteLine("namespace QuickBooksSharp.Entities");
242 | writer.WriteLine("{");
243 |
244 | foreach (var e in enums)
245 | {
246 | writer.WriteLine($"public enum {e.Name}");
247 | writer.WriteLine("{");
248 |
249 | writer.WriteLine($"Unspecified = 0,");
250 | foreach (var name in e.Fields)
251 | {
252 | string safeName = GetSafePropertyName(name);
253 | if (name != safeName)
254 | writer.WriteLine($"[EnumMember(Value = \"{name}\")]");
255 | writer.WriteLine($"{safeName},");
256 | }
257 |
258 | writer.WriteLine("}");
259 | }
260 |
261 | foreach (var c in classes)
262 | {
263 | writer.Write($"public {(c.IsAbstract ? "abstract" : "")} class {GetSafeClassName(c.Name)}");
264 | if (c.BaseName != null)
265 | writer.Write($" : {c.BaseName}");
266 | writer.WriteLine();
267 | writer.WriteLine("{");
268 |
269 | foreach (var pty in c.Properties)
270 | {
271 | string safeName = GetSafePropertyName(pty.Name);
272 | if (pty.Name != safeName)
273 | writer.WriteLine($"[JsonPropertyName(\"{pty.Name}\")]");
274 |
275 | if (c.Name == "BatchItemRequest")
276 | writer.WriteLine(pty.Name == "IntuitObject" ? "[JsonIgnore]" : "[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]");
277 |
278 | static string GetPropertyDeclaration(PropertyModel pty, string safeName, string typeName)
279 | {
280 | string ptyDecl = string.Empty;
281 | ptyDecl += $"public {typeName}";
282 | if (pty.IsArray)
283 | ptyDecl += "[]";
284 | if (pty.IsNullable)
285 | ptyDecl += "?";
286 | ptyDecl += $" {safeName} ";
287 | ptyDecl += pty.Code ?? "{ get; set; }";
288 | if (!pty.IsNullable && pty.Code == null)
289 | ptyDecl += " = default!; ";
290 |
291 | return ptyDecl;
292 | }
293 | string typeName = GetSafeClassName(pty.TypeName);
294 | if (typeName != nameof(DateOnly))
295 | writer.WriteLine(GetPropertyDeclaration(pty, safeName, typeName));
296 | else
297 | {
298 | //DateOnly type is only support on .NET6+
299 | writer.WriteLine("#if NET6_0_OR_GREATER");
300 | writer.WriteLine(GetPropertyDeclaration(pty, safeName, typeName));
301 | writer.WriteLine("#else");
302 | writer.WriteLine(GetPropertyDeclaration(pty, safeName, "DateTime"));
303 | writer.WriteLine("#endif");
304 | }
305 |
306 | }
307 |
308 | writer.WriteLine("}");
309 | }
310 |
311 | writer.WriteLine("}");
312 | }
313 | }
314 |
315 | private static void FormatOutFile(string outFilePath)
316 | {
317 | string rawText = File.ReadAllText(outFilePath);
318 | var ws = new AdhocWorkspace();
319 | var code = CSharpSyntaxTree.ParseText(rawText);
320 | string formattedText = Formatter.Format(code.GetRoot(), ws).ToFullString();
321 | File.WriteAllText(outFilePath, formattedText);
322 | }
323 |
324 | private static string GetSafeClassName(string name)
325 | {
326 | //Avoid name conflict with System.Threading.Tasks.Task
327 | return name == "Task" ? "QbTask" : name;
328 | }
329 |
330 | private static string GetSafePropertyName(string name)
331 | {
332 | //must escape property names that conflict with keywords
333 | if (new[] { "void" }.Contains(name))
334 | return $"@{name}";
335 |
336 | return Regex.Replace(name, "[-%)( ]", string.Empty);
337 | }
338 | }
339 | }
340 |
--------------------------------------------------------------------------------
/QuickBooksSharp.CodeGen/QuickBooksSharp.CodeGen.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net9.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/QuickBooksSharp.CodeGen/xsd/3.56/Report.xsd:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 | Date macros enumeration
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | Specifies the column type definition
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | List of all row types
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
94 |
95 | Describes the type
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | Describes the Name
105 |
106 |
107 |
108 |
109 | Describes the Value
110 |
111 |
112 |
113 |
114 |
115 |
116 | List of columns
117 |
118 |
119 |
121 |
122 | Column of the report
123 |
124 |
125 |
126 |
127 |
128 |
129 | Describes a column
130 |
131 |
132 |
133 |
134 | Describes the column title name
135 |
136 |
137 |
138 |
139 | Describes the column type enumeration
140 |
141 |
142 |
143 |
144 | Column Metadata
145 |
146 |
147 |
148 |
149 |
150 | Subcolumns of the column
151 |
152 |
153 |
154 |
155 |
156 |
157 | One ColData can contain one column
158 |
159 |
160 |
161 |
162 | Describes the column attributes
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | Reference url
171 |
172 |
173 |
174 |
175 |
176 | One Row can contain any number of columns
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 | Row type section, summary, data row etc..
197 |
198 |
199 |
200 |
201 | Report Group Income, Expense, COGS etc..
202 |
203 |
204 |
205 |
206 |
207 | Group Header
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 | Group Summary
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 | List of rows
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 | Specifies the Header of a Report, Time report was generated, parameters corresponding to the request
232 |
233 |
234 |
235 |
236 | Specifies the time at which report was generated
237 |
238 |
239 |
240 |
241 | Specifies the report name
242 |
243 |
244 |
245 |
246 | Specifies the report name
247 |
248 |
249 |
250 |
251 | Specifies the report is cash basis or accrual basis
252 |
253 |
254 |
255 |
256 | Start Period for which the report was generated
257 |
258 |
259 |
260 |
261 | End Period for which the report was generated
262 |
263 |
264 |
265 |
266 | Summarize columns by enumeration
267 |
268 |
269 |
270 |
271 | Specifies the currency code associated with the report, note that this is one place where this is just the currency code, not a reference to a currency object
272 |
273 |
274 |
275 |
276 | Specifies the customer id (comma separeted) for which the report is run this is just the id, not a reference to a customer object
277 |
278 |
279 |
280 |
281 | Specifies the vendor id (comma separeted) for which the report is run this is just the id, not a reference to a vendor object
282 |
283 |
284 |
285 |
286 | Specifies the employee id (comma separeted) for which the report is run this is just the id, not a reference to a employee object
287 |
288 |
289 |
290 |
291 | Specifies the product/service id (comma separeted) for which the report is run this is just the id, not a reference to a product/service object
292 |
293 |
294 |
295 |
296 | Specifies the class id (comma separeted) for which the report is run this is just the id, not a reference to a class object
297 |
298 |
299 |
300 |
301 | Specifies the Department id (comma separeted) for which the report is run this is just the id, not a reference to a Department object
302 |
303 |
304 |
305 |
306 |
307 | Describes the options used for the report
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 | Report Response Type
320 |
321 |
322 |
323 |
324 | Report Header, contains the report options that were used to generate the report
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
--------------------------------------------------------------------------------
/QuickBooksSharp.CodeGen/xsd/3.56/SalesTax.xsd:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 | Product: QBO
10 | Description: TaxRate details
11 |
12 |
13 |
14 |
15 |
16 |
17 | Product: QBO
18 | Description: TaxRate details
19 |
20 |
21 |
22 |
23 |
24 |
25 | Product: QBO
26 | Description: TaxRate details
27 |
28 |
29 |
30 |
31 |
32 |
33 | Product: QBO
34 | Description: TaxRate value
35 |
36 |
37 |
38 |
39 |
40 |
41 | Product: QBO
42 | Description: TaxAgency details
43 |
44 |
45 |
46 |
47 |
48 |
49 | Product: QBO
50 | Description: Default is SalesTax
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | Describes SalesTax details
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | Product: QBO
68 | Description: Describes the taxcode
69 |
70 |
71 |
72 |
73 |
74 |
75 | Product: QBO
76 | Description: Describes the taxcode Id, this is output only
77 |
78 |
79 |
80 |
81 |
82 |
83 | Product: QBO
84 | Description: TaxRate details
85 |
86 |
87 |
88 |
89 |
90 | Fault or Object should be returned
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | Product: QBO
102 | Description: Enumeration of transaction type a given tax rate can be applied to
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/QuickBooksSharp.CodeGen/xsd/3.65/Report.xsd:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 | Date macros enumeration
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | Specifies the column type definition
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | List of all row types
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
94 |
95 | Describes the type
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | Describes the Name
105 |
106 |
107 |
108 |
109 | Describes the Value
110 |
111 |
112 |
113 |
114 |
115 |
116 | List of columns
117 |
118 |
119 |
121 |
122 | Column of the report
123 |
124 |
125 |
126 |
127 |
128 |
129 | Describes a column
130 |
131 |
132 |
133 |
134 | Describes the column title name
135 |
136 |
137 |
138 |
139 | Describes the column type enumeration
140 |
141 |
142 |
143 |
144 | Column Metadata
145 |
146 |
147 |
148 |
149 |
150 | Subcolumns of the column
151 |
152 |
153 |
154 |
155 |
156 |
157 | One ColData can contain one column
158 |
159 |
160 |
161 |
162 | Describes the column attributes
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | Reference url
171 |
172 |
173 |
174 |
175 |
176 | One Row can contain any number of columns
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 | Row type section, summary, data row etc..
197 |
198 |
199 |
200 |
201 | Report Group Income, Expense, COGS etc..
202 |
203 |
204 |
205 |
206 |
207 | Group Header
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 | Group Summary
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 | List of rows
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 | Specifies the Header of a Report, Time report was generated, parameters corresponding to the request
232 |
233 |
234 |
235 |
236 | Specifies the time at which report was generated
237 |
238 |
239 |
240 |
241 | Specifies the report name
242 |
243 |
244 |
245 |
246 | Specifies the report name
247 |
248 |
249 |
250 |
251 | Specifies the report is cash basis or accrual basis
252 |
253 |
254 |
255 |
256 | Start Period for which the report was generated
257 |
258 |
259 |
260 |
261 | End Period for which the report was generated
262 |
263 |
264 |
265 |
266 | Summarize columns by enumeration
267 |
268 |
269 |
270 |
271 | Specifies the currency code associated with the report, note that this is one place where this is just the currency code, not a reference to a currency object
272 |
273 |
274 |
275 |
276 | Specifies the customer id (comma separeted) for which the report is run this is just the id, not a reference to a customer object
277 |
278 |
279 |
280 |
281 | Specifies the vendor id (comma separeted) for which the report is run this is just the id, not a reference to a vendor object
282 |
283 |
284 |
285 |
286 | Specifies the employee id (comma separeted) for which the report is run this is just the id, not a reference to a employee object
287 |
288 |
289 |
290 |
291 | Specifies the product/service id (comma separeted) for which the report is run this is just the id, not a reference to a product/service object
292 |
293 |
294 |
295 |
296 | Specifies the class id (comma separeted) for which the report is run this is just the id, not a reference to a class object
297 |
298 |
299 |
300 |
301 | Specifies the Department id (comma separeted) for which the report is run this is just the id, not a reference to a Department object
302 |
303 |
304 |
305 |
306 |
307 | Describes the options used for the report
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 | Report Response Type
320 |
321 |
322 |
323 |
324 | Report Header, contains the report options that were used to generate the report
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
--------------------------------------------------------------------------------
/QuickBooksSharp.CodeGen/xsd/3.65/SalesTax.xsd:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 | Product: QBO
10 | Description: TaxRate details
11 |
12 |
13 |
14 |
15 |
16 |
17 | Product: QBO
18 | Description: TaxRate details
19 |
20 |
21 |
22 |
23 |
24 |
25 | Product: QBO
26 | Description: TaxRate details
27 |
28 |
29 |
30 |
31 |
32 |
33 | Product: QBO
34 | Description: TaxRate value
35 |
36 |
37 |
38 |
39 |
40 |
41 | Product: QBO
42 | Description: TaxAgency details
43 |
44 |
45 |
46 |
47 |
48 |
49 | Product: QBO
50 | Description: Default is SalesTax
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | Describes SalesTax details
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | Product: QBO
68 | Description: Describes the taxcode
69 |
70 |
71 |
72 |
73 |
74 |
75 | Product: QBO
76 | Description: Describes the taxcode Id, this is output only
77 |
78 |
79 |
80 |
81 |
82 |
83 | Product: QBO
84 | Description: TaxRate details
85 |
86 |
87 |
88 |
89 |
90 | Fault or Object should be returned
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | Product: QBO
102 | Description: Enumeration of transaction type a given tax rate can be applied to
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/QuickBooksSharp.CodeGen/xsd/3.73/Report.xsd:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 | Date macros enumeration
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | Specifies the column type definition
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | List of all row types
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
94 |
95 | Describes the type
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | Describes the Name
105 |
106 |
107 |
108 |
109 | Describes the Value
110 |
111 |
112 |
113 |
114 |
115 |
116 | List of columns
117 |
118 |
119 |
121 |
122 | Column of the report
123 |
124 |
125 |
126 |
127 |
128 |
129 | Describes a column
130 |
131 |
132 |
133 |
134 | Describes the column title name
135 |
136 |
137 |
138 |
139 | Describes the column type enumeration
140 |
141 |
142 |
143 |
144 | Column Metadata
145 |
146 |
147 |
148 |
149 |
150 | Subcolumns of the column
151 |
152 |
153 |
154 |
155 |
156 |
157 | One ColData can contain one column
158 |
159 |
160 |
161 |
162 | Describes the column attributes
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | Reference url
171 |
172 |
173 |
174 |
175 |
176 | One Row can contain any number of columns
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 | Row type section, summary, data row etc..
197 |
198 |
199 |
200 |
201 | Report Group Income, Expense, COGS etc..
202 |
203 |
204 |
205 |
206 |
207 | Group Header
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 | Group Summary
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 | List of rows
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 | Specifies the Header of a Report, Time report was generated, parameters corresponding to the request
232 |
233 |
234 |
235 |
236 | Specifies the time at which report was generated
237 |
238 |
239 |
240 |
241 | Specifies the report name
242 |
243 |
244 |
245 |
246 | Specifies the report name
247 |
248 |
249 |
250 |
251 | Specifies the report is cash basis or accrual basis
252 |
253 |
254 |
255 |
256 | Start Period for which the report was generated
257 |
258 |
259 |
260 |
261 | End Period for which the report was generated
262 |
263 |
264 |
265 |
266 | Summarize columns by enumeration
267 |
268 |
269 |
270 |
271 | Specifies the currency code associated with the report, note that this is one place where this is just the currency code, not a reference to a currency object
272 |
273 |
274 |
275 |
276 | Specifies the customer id (comma separeted) for which the report is run this is just the id, not a reference to a customer object
277 |
278 |
279 |
280 |
281 | Specifies the vendor id (comma separeted) for which the report is run this is just the id, not a reference to a vendor object
282 |
283 |
284 |
285 |
286 | Specifies the employee id (comma separeted) for which the report is run this is just the id, not a reference to a employee object
287 |
288 |
289 |
290 |
291 | Specifies the product/service id (comma separeted) for which the report is run this is just the id, not a reference to a product/service object
292 |
293 |
294 |
295 |
296 | Specifies the class id (comma separeted) for which the report is run this is just the id, not a reference to a class object
297 |
298 |
299 |
300 |
301 | Specifies the Department id (comma separeted) for which the report is run this is just the id, not a reference to a Department object
302 |
303 |
304 |
305 |
306 |
307 | Describes the options used for the report
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 | Report Response Type
320 |
321 |
322 |
323 |
324 | Report Header, contains the report options that were used to generate the report
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
--------------------------------------------------------------------------------
/QuickBooksSharp.CodeGen/xsd/3.73/SalesTax.xsd:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 | Product: QBO
10 | Description: TaxRate details
11 |
12 |
13 |
14 |
15 |
16 |
17 | Product: QBO
18 | Description: TaxRate details
19 |
20 |
21 |
22 |
23 |
24 |
25 | Product: QBO
26 | Description: TaxRate details
27 |
28 |
29 |
30 |
31 |
32 |
33 | Product: QBO
34 | Description: TaxRate value
35 |
36 |
37 |
38 |
39 |
40 |
41 | Product: QBO
42 | Description: TaxAgency details
43 |
44 |
45 |
46 |
47 |
48 |
49 | Product: QBO
50 | Description: Default is SalesTax
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | Describes SalesTax details
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | Product: QBO
68 | Description: Describes the taxcode
69 |
70 |
71 |
72 |
73 |
74 |
75 | Product: QBO
76 | Description: Describes the taxcode Id, this is output only
77 |
78 |
79 |
80 |
81 |
82 |
83 | Product: QBO
84 | Description: TaxRate details
85 |
86 |
87 |
88 |
89 |
90 | Fault or Object should be returned
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | Product: QBO
102 | Description: Enumeration of transaction type a given tax rate can be applied to
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/QuickBooksSharp.CodeGen/xsd/3.75/Report.xsd:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 | Date macros enumeration
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | Specifies the column type definition
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | List of all row types
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
94 |
95 | Describes the type
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | Describes the Name
105 |
106 |
107 |
108 |
109 | Describes the Value
110 |
111 |
112 |
113 |
114 |
115 |
116 | List of columns
117 |
118 |
119 |
121 |
122 | Column of the report
123 |
124 |
125 |
126 |
127 |
128 |
129 | Describes a column
130 |
131 |
132 |
133 |
134 | Describes the column title name
135 |
136 |
137 |
138 |
139 | Describes the column type enumeration
140 |
141 |
142 |
143 |
144 | Column Metadata
145 |
146 |
147 |
148 |
149 |
150 | Subcolumns of the column
151 |
152 |
153 |
154 |
155 |
156 |
157 | One ColData can contain one column
158 |
159 |
160 |
161 |
162 | Describes the column attributes
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | Reference url
171 |
172 |
173 |
174 |
175 |
176 | One Row can contain any number of columns
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 | Row type section, summary, data row etc..
197 |
198 |
199 |
200 |
201 | Report Group Income, Expense, COGS etc..
202 |
203 |
204 |
205 |
206 |
207 | Group Header
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 | Group Summary
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 | List of rows
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 | Specifies the Header of a Report, Time report was generated, parameters corresponding to the request
232 |
233 |
234 |
235 |
236 | Specifies the time at which report was generated
237 |
238 |
239 |
240 |
241 | Specifies the report name
242 |
243 |
244 |
245 |
246 | Specifies the report name
247 |
248 |
249 |
250 |
251 | Specifies the report is cash basis or accrual basis
252 |
253 |
254 |
255 |
256 | Start Period for which the report was generated
257 |
258 |
259 |
260 |
261 | End Period for which the report was generated
262 |
263 |
264 |
265 |
266 | Summarize columns by enumeration
267 |
268 |
269 |
270 |
271 | Specifies the currency code associated with the report, note that this is one place where this is just the currency code, not a reference to a currency object
272 |
273 |
274 |
275 |
276 | Specifies the customer id (comma separeted) for which the report is run this is just the id, not a reference to a customer object
277 |
278 |
279 |
280 |
281 | Specifies the vendor id (comma separeted) for which the report is run this is just the id, not a reference to a vendor object
282 |
283 |
284 |
285 |
286 | Specifies the employee id (comma separeted) for which the report is run this is just the id, not a reference to a employee object
287 |
288 |
289 |
290 |
291 | Specifies the product/service id (comma separeted) for which the report is run this is just the id, not a reference to a product/service object
292 |
293 |
294 |
295 |
296 | Specifies the class id (comma separeted) for which the report is run this is just the id, not a reference to a class object
297 |
298 |
299 |
300 |
301 | Specifies the Department id (comma separeted) for which the report is run this is just the id, not a reference to a Department object
302 |
303 |
304 |
305 |
306 |
307 | Describes the options used for the report
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 | Report Response Type
320 |
321 |
322 |
323 |
324 | Report Header, contains the report options that were used to generate the report
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
--------------------------------------------------------------------------------
/QuickBooksSharp.CodeGen/xsd/3.75/SalesTax.xsd:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 | Product: QBO
10 | Description: TaxRate details
11 |
12 |
13 |
14 |
15 |
16 |
17 | Product: QBO
18 | Description: TaxRate details
19 |
20 |
21 |
22 |
23 |
24 |
25 | Product: QBO
26 | Description: TaxRate details
27 |
28 |
29 |
30 |
31 |
32 |
33 | Product: QBO
34 | Description: TaxRate value
35 |
36 |
37 |
38 |
39 |
40 |
41 | Product: QBO
42 | Description: TaxAgency details
43 |
44 |
45 |
46 |
47 |
48 |
49 | Product: QBO
50 | Description: Default is SalesTax
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | Describes SalesTax details
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | Product: QBO
68 | Description: Describes the taxcode
69 |
70 |
71 |
72 |
73 |
74 |
75 | Product: QBO
76 | Description: Describes the taxcode Id, this is output only
77 |
78 |
79 |
80 |
81 |
82 |
83 | Product: QBO
84 | Description: TaxRate details
85 |
86 |
87 |
88 |
89 |
90 | Fault or Object should be returned
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | Product: QBO
102 | Description: Enumeration of transaction type a given tax rate can be applied to
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/QuickBooksSharp.Tests/AuthenticationService_Tests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using System;
3 | using System.Threading.Tasks;
4 |
5 | namespace QuickBooksSharp.Tests
6 | {
7 | [TestClass]
8 | public class AuthenticationService_Tests
9 | {
10 | private AuthenticationService _service;
11 |
12 | [TestInitialize]
13 | public void Initialize()
14 | {
15 | _service = new AuthenticationService();
16 | }
17 |
18 | [TestMethod]
19 | public void GenerateAuthorizationPromptUrl_Works()
20 | {
21 | string url = _service.GenerateAuthorizationPromptUrl(TestHelper.ClientId, new[] { "com.intuit.quickbooks.accounting" }, TestHelper.RedirectUri, Guid.NewGuid().ToString());
22 | Assert.IsNotNull(url);
23 | }
24 |
25 | [TestMethod]
26 | [Ignore("Requires manual input of code")]
27 | public async Task GetOAuthTokenAsync_Works()
28 | {
29 | var token = await _service.GetOAuthTokenAsync(TestHelper.ClientId, TestHelper.ClientSecret, "", TestHelper.RedirectUri);
30 | Assert.IsNotNull(token.access_token);
31 | Assert.IsNotNull(token.refresh_token);
32 | Assert.IsTrue(token.expires_in > TimeSpan.Zero);
33 | Assert.IsTrue(token.x_refresh_token_expires_in > TimeSpan.Zero);
34 | }
35 |
36 |
37 | [TestMethod]
38 | public async Task RefreshOAuthTokenAsync_Works()
39 | {
40 | var token = await _service.RefreshOAuthTokenAsync(TestHelper.ClientId, TestHelper.ClientSecret, TestHelper.RefreshToken);
41 | Assert.IsNotNull(token.access_token);
42 | Assert.IsTrue(token.expires_in > TimeSpan.Zero);
43 | Assert.IsNotNull(token.refresh_token);
44 | }
45 |
46 | [TestMethod]
47 | [Ignore("Requires manual input of token")]
48 | public async Task RevokeOAuthTokenAsync_Works()
49 | {
50 | await _service.RevokeOAuthTokenAsync(TestHelper.ClientId, TestHelper.ClientSecret, "");
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/QuickBooksSharp.Tests/DataServiceTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using QuickBooksSharp.Entities;
3 | using System;
4 | using System.Collections.Concurrent;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Threading.Tasks;
8 |
9 | namespace QuickBooksSharp.Tests
10 | {
11 | [TestClass]
12 | public class DataServiceTests : ServiceTestBase
13 | {
14 | private DataService _service;
15 | private readonly Type[] _entityTypes = new[]
16 | {
17 | typeof(Account),
18 | typeof(BillPayment),
19 | typeof(Bill),
20 | typeof(Budget),
21 | typeof(Class),
22 | typeof(CompanyCurrency),
23 | typeof(CompanyInfo),
24 | typeof(CreditCardPayment),
25 | typeof(CreditMemo),
26 | typeof(Customer),
27 | typeof(CustomerType),
28 | typeof(Deposit),
29 | typeof(Employee),
30 | typeof(Estimate),
31 | typeof(ExchangeRate),
32 | typeof(Invoice),
33 | typeof(Item),
34 | typeof(JournalEntry),
35 | typeof(Department),
36 | typeof(PaymentMethod),
37 | typeof(Payment),
38 | typeof(Preferences),
39 | typeof(PurchaseOrder),
40 | typeof(Purchase),
41 | typeof(RefundReceipt),
42 | typeof(ReimburseCharge),
43 | typeof(SalesReceipt),
44 | typeof(TaxAgency),
45 | typeof(TaxClassification),
46 | typeof(TaxCode),
47 | typeof(TaxRate),
48 | typeof(Term),
49 | typeof(TimeActivity),
50 | typeof(Transfer),
51 | typeof(VendorCredit),
52 | typeof(Vendor),
53 | };
54 |
55 | [TestInitialize]
56 | public async Task Initialize()
57 | {
58 | var accessToken = await GetAccessTokenAsync();
59 | _service = new DataService(accessToken, TestHelper.RealmId, true);
60 | }
61 |
62 | [TestMethod]
63 | public async Task QueryCustomers()
64 | {
65 | var res = await _service.QueryAsync("SELECT * FROM Customer");
66 | Assert.IsNotNull(res);
67 | Assert.IsNull(res.Fault);
68 | Assert.IsNotNull(res.Time);
69 | Assert.IsNotNull(res.Response);
70 | Assert.IsNull(res.Response.Fault);
71 | Assert.IsNotNull(res.Response.StartPosition);
72 | Assert.IsNotNull(res.Response.Entities);
73 | Assert.IsNotNull(res.Response.MaxResults);
74 | Assert.IsNotNull(res.Response.Entities[0].Id);
75 | Assert.IsNotNull(res.Response.Entities[0].DisplayName);
76 | }
77 |
78 | [TestMethod]
79 | public async Task QueryCustomerCount()
80 | {
81 | var res = await _service.QueryCountAsync("SELECT COUNT(*) FROM Customer");
82 | Assert.IsNotNull(res);
83 | Assert.IsNull(res.Fault);
84 | Assert.IsNotNull(res.Time);
85 | Assert.IsNotNull(res.Response);
86 | Assert.IsNull(res.Response.Fault);
87 | Assert.IsNotNull(res.Response.TotalCount);
88 | }
89 |
90 | [TestMethod]
91 | public async Task CreateUpdateParseUpdateCustomer()
92 | {
93 | string uniquifier = DateTime.Now.Ticks.ToString();
94 | var resCreate = await _service.PostAsync(new Customer
95 | {
96 | DisplayName = $"Test - My display name {uniquifier}",
97 | Suffix = "Jr",
98 | Title = "Mr",
99 | MiddleName = "Test - My middle name",
100 | FamilyName = "Test - My family name",
101 | GivenName = "Test - My given name",
102 | });
103 | Assert.IsNotNull(resCreate);
104 | Assert.IsNull(resCreate.Fault);
105 | Assert.IsNotNull(resCreate.Time);
106 | Assert.IsNotNull(resCreate.Response);
107 | Assert.IsNotNull(resCreate.Response.Id);
108 | Assert.IsNotNull(resCreate.Response.DisplayName);
109 |
110 | var resSparseUpdate = await _service.PostAsync(new Customer
111 | {
112 | Id = resCreate.Response.Id,
113 | SyncToken = resCreate.Response.SyncToken,
114 | GivenName = $"{resCreate.Response.GivenName} - sparsed",
115 | sparse = true
116 | });
117 | Assert.IsNotNull(resSparseUpdate);
118 | Assert.IsNull(resSparseUpdate.Fault);
119 | Assert.IsNotNull(resSparseUpdate.Time);
120 | Assert.IsNotNull(resSparseUpdate.Response);
121 | Assert.IsNotNull(resSparseUpdate.Response.Id);
122 | Assert.AreEqual(resSparseUpdate.Response.DisplayName, resCreate.Response.DisplayName);
123 | Assert.AreNotEqual(resSparseUpdate.Response.GivenName, resCreate.Response.GivenName);
124 |
125 | var c = resSparseUpdate.Response;
126 | c.FamilyName = $"{resSparseUpdate.Response.FamilyName} - full";
127 | c.sparse = false;
128 | var resFullUpdate = await _service.PostAsync(resSparseUpdate.Response);
129 | Assert.IsNotNull(resFullUpdate);
130 | Assert.IsNull(resFullUpdate.Fault);
131 | Assert.IsNotNull(resFullUpdate.Time);
132 | Assert.IsNotNull(resFullUpdate.Response);
133 | Assert.IsNotNull(resFullUpdate.Response.Id);
134 | Assert.AreEqual(resFullUpdate.Response.DisplayName, resCreate.Response.DisplayName);
135 | Assert.AreNotEqual(resFullUpdate.Response.FamilyName, resCreate.Response.FamilyName);
136 | }
137 |
138 | [TestMethod]
139 | public async Task CreateDeleteInvoice()
140 | {
141 | var queryCustomerRes = await _service.QueryAsync("SELECT * FROM Customer MAXRESULTS 1");
142 | var queryAccountRes = await _service.QueryAsync("SELECT * FROM Account MAXRESULTS 1");
143 |
144 | var resCreate = await _service.PostAsync(new Invoice
145 | {
146 | CustomerRef = new ReferenceType
147 | {
148 | value = queryCustomerRes.Response.Entities[0].Id
149 | },
150 | Line = new[]
151 | {
152 | new Line
153 | {
154 | Amount = 100.0m,
155 | DetailType = LineDetailTypeEnum.SalesItemLineDetail,
156 | SalesItemLineDetail = new SalesItemLineDetail
157 | {
158 | ItemAccountRef = new ReferenceType
159 | {
160 | value = queryAccountRes.Response.Entities[0].Id
161 | }
162 | }
163 | }
164 | }
165 | });
166 | Assert.IsNotNull(resCreate);
167 | Assert.IsNull(resCreate.Fault);
168 | Assert.IsNotNull(resCreate.Time);
169 | Assert.IsNotNull(resCreate.Response);
170 | Assert.IsNotNull(resCreate.Response.Id);
171 |
172 | var resDelete = await _service.PostAsync(new Invoice { Id = resCreate.Response.Id, SyncToken = resCreate.Response.SyncToken }, OperationEnum.delete);
173 | }
174 |
175 | [TestMethod]
176 | public async Task QueryEntitiesCount()
177 | {
178 | await Task.WhenAll(_entityTypes
179 | .Where(t2 => !new[]
180 | {
181 | typeof(TaxPayment),//Only available on AU/UK companies
182 | typeof(ExchangeRate),//Message=Error processing query https://help.developer.intuit.com/s/question/0D54R00007pirJESAY/the-following-query-results-in-an-error-select-count-from-exchangerateerror-returned-from-api-error-processing-query
183 | typeof(CustomerType),//Detail=Dear entity developer, pl implement count query https://help.developer.intuit.com/s/question/0D54R00007pirJFSAY/select-count-from-customertype-returns-an-error
184 | typeof(CompanyInfo),//Total count not returned
185 | }.Contains(t2))
186 | .Select(async t =>
187 | {
188 | try
189 | {
190 | string entityName = t == typeof(QbTask) ? "Task" : t.Name;
191 | var res = await _service.QueryCountAsync($"SELECT COUNT(*) FROM {entityName}");
192 | Assert.IsNotNull(res);
193 | Assert.IsNull(res.Fault);
194 | Assert.IsNotNull(res.Time);
195 | Assert.IsNotNull(res.Response);
196 | Assert.IsNull(res.Response.Fault);
197 | Assert.IsNotNull(res.Response.TotalCount);
198 | }
199 | catch (QuickBooksException ex) when (ex.ResponseContent.Contains("Metadata not found for Entity"))
200 | {
201 | //Ignore entities that don't support querying
202 | }
203 | }));
204 | }
205 |
206 | [TestMethod]
207 | public async Task QueryEntities()
208 | {
209 | var entities = new ConcurrentQueue();
210 | await Task.WhenAll(_entityTypes
211 | .Where(t2 => !new[]
212 | {
213 | typeof(Employee),//API error complaing about inactive employee
214 | }.Contains(t2))
215 | .Select(async t =>
216 | {
217 | try
218 | {
219 | string entityName = t == typeof(QbTask) ? "Task" : t.Name;
220 | var res = await _service.QueryAsync($"SELECT * FROM {entityName}");
221 | Assert.IsNotNull(res);
222 | Assert.IsNull(res.Fault);
223 | Assert.IsNotNull(res.Time);
224 | Assert.IsNull(res.Response.Fault);
225 | Assert.IsNotNull(res.Response);
226 | //it seems that if there are 0 rows, the following are null
227 | if (res.Response.StartPosition != null)
228 | {
229 | Assert.IsNotNull(res.Response.Entities);
230 | Assert.IsNotNull(res.Response.MaxResults);
231 | if (res.Response.Entities.FirstOrDefault()?.Id != null)
232 | {
233 | //Built-in tax code entities can have non numeric id TAX or NON
234 | //See https://help.developer.intuit.com/s/question/0D74R000004jvUi
235 | entities.Enqueue(res.Response.Entities.FirstOrDefault(i => long.TryParse(i.Id, out _)));
236 | }
237 | }
238 | }
239 | catch (QuickBooksException ex) when (ex.ResponseContent.Contains("Metadata not found for Entity"))
240 | {
241 | //Ignore entities that don't support querying
242 | }
243 | }));
244 |
245 | await Task.WhenAll(entities
246 | //https://help.developer.intuit.com/s/question/0D54R00007pisJuSAI/taxcode-id-tax-instead-of-numeric-ids
247 | .Select(async e =>
248 | {
249 | var resOne = await _service.GetAsync(e.Id, e.GetType());
250 | Assert.IsNotNull(resOne);
251 | Assert.IsNotNull(resOne.Response);
252 | Assert.IsNotNull(resOne.Response.Id);
253 | }));
254 | }
255 |
256 | [TestMethod]
257 | public async Task GetProfitAndLossReport()
258 | {
259 | var r = await _service.GetReportAsync("ProfitAndLoss", new()
260 | {
261 | { "accounting_method", "Accrual" },
262 | { "date_macro", "Last Fiscal Year" }
263 | });
264 | Assert.IsNotNull(r);
265 | Assert.IsNotNull(r.Header?.ReportName);
266 | Assert.IsTrue(r.Rows.Row.Length != 0);
267 | Assert.IsTrue(r.Columns.Column.Length != 0);
268 | }
269 |
270 | [TestMethod]
271 | public async Task GetJournalReport()
272 | {
273 | var r = await _service.GetReportAsync("JournalReport", new()
274 | {
275 | { "date_macro", "Last Fiscal Year" }
276 | });
277 | Assert.IsNotNull(r);
278 | Assert.IsNotNull(r.Header?.ReportName);
279 | Assert.IsTrue(r.Rows.Row.Length != 0);
280 | Assert.IsTrue(r.Columns.Column.Length != 0);
281 | }
282 |
283 | [TestMethod]
284 | public async Task GetTransactionDetailByAccount()
285 | {
286 | var includedColumns = new[]
287 | {
288 | "account_name",
289 | "create_by",
290 | "create_date",
291 | "credit_amt",
292 | "credit_home_amt",
293 | "currency",
294 | "cust_name",
295 | "debt_amt",
296 | "debt_home_amt",
297 | "dept_name",
298 | "doc_num",
299 | "due_date",
300 | "emp_name",
301 | "exch_rate",
302 | "foreign_net_amount",
303 | "foreign_tax_amount",
304 | "home_net_amount",
305 | "home_tax_amount",
306 | "net_amount",
307 | "tax_amount",
308 | "is_adj",
309 | "is_ap_paid",
310 | "is_ar_paid",
311 | "is_cleared",
312 | "item_name",
313 | "klass_name",
314 | "last_mod_by",
315 | "last_mod_date",
316 | "memo",
317 | "nat_foreign_amount",
318 | "nat_foreign_open_bal",
319 | "nat_home_open_bal",
320 | "nat_open_bal",
321 | "olb_status",
322 | "pmt_mthd",
323 | "quantity",
324 | "rate",
325 | "split_acc",
326 | "subt_nat_home_amount",
327 | "subt_nat_amount",
328 | "tax_code_name",
329 | "tax_type",
330 | "tx_date",
331 | "txn_type",
332 | "vend_name",
333 | };
334 | var r = await _service.GetReportAsync("TransactionDetailByAccount", new()
335 | {
336 | { "columns", string.Join(",", includedColumns) },
337 | { "transaction_type", "post" },
338 | { "groupby", "none" },
339 | { "accounting_method", "Cash" },
340 | { "sort_by", "create_date" },
341 | { "sort_order", "descend" },
342 | { "start_date", DateTime.Today.AddMonths(-1).ToString("yyyy-MM-dd") },
343 | { "end_date", DateTime.Today.ToString("yyyy-MM-dd") },
344 | });
345 | Assert.IsNotNull(r);
346 | Assert.IsNotNull(r.Header?.ReportName);
347 | Assert.IsTrue(r.Columns.Column.Length != 0);
348 | }
349 |
350 |
351 | [TestMethod]
352 | public async Task GetCDC()
353 | {
354 | var entityTypes = _entityTypes.Except(new[]
355 | {
356 | typeof(TaxPayment), //UK/AU only
357 |
358 | //not all entities are supported by CDC
359 | typeof(QbdtEntityIdMapping),
360 | typeof(ConvenienceFeeDetail),
361 | typeof(EmailDeliveryInfo),
362 | typeof(Tag),
363 | typeof(FixedAsset),
364 | typeof(MasterAccount),
365 | typeof(StatementCharge),
366 | typeof(JournalCode),
367 | typeof(SalesOrder),
368 | typeof(SalesRep),
369 | typeof(PriceLevel),
370 | typeof(PriceLevelPerItem),
371 | typeof(CustomerMsg),
372 | typeof(InventorySite),
373 | typeof(ShipMethod),
374 | typeof(QbTask),
375 | typeof(UOM),
376 | typeof(TemplateName),
377 | typeof(TDSMetadata),
378 | typeof(BooleanTypeCustomFieldDefinition),
379 | typeof(DateTypeCustomFieldDefinition),
380 | typeof(NumberTypeCustomFieldDefinition),
381 | typeof(StringTypeCustomFieldDefinition),
382 | typeof(ChargeCredit),
383 | typeof(JobType),
384 | typeof(OtherName),
385 | typeof(Status),
386 | typeof(SyncActivity),
387 | typeof(TaxAgency),
388 | typeof(TaxClassification),
389 | typeof(TaxService),
390 | typeof(User),
391 | typeof(VendorType),
392 | typeof(Currency),
393 | })
394 | .OrderBy(t => t.Name);
395 |
396 | await Task.WhenAll(entityTypes.Select(async t =>
397 | {
398 | var res = await _service.GetCDCAsync(DateTimeOffset.UtcNow.AddDays(-10), new[] { t.Name });
399 | Assert.IsNotNull(res);
400 | Assert.IsNotNull(res.Response);
401 | Assert.IsTrue(res.Response.QueryResponse.Length == 1);
402 | var queryResponse = res.Response.QueryResponse.First();
403 | if (queryResponse.IntuitObjects != null)
404 | Assert.IsTrue(queryResponse.IntuitObjects.All(o => o.GetType() == t));
405 | }));
406 |
407 | var resAll = await _service.GetCDCAsync(DateTimeOffset.UtcNow.AddDays(-10), entityTypes.Select(t => t.Name));
408 | Assert.IsNotNull(resAll);
409 | Assert.IsNotNull(resAll.Response);
410 | Assert.IsTrue(resAll.Response.QueryResponse.Length == entityTypes.Count());
411 | }
412 |
413 | [TestMethod]
414 | public async Task BatchQuery()
415 | {
416 | var response = await _service.BatchAsync(new IntuitBatchRequest
417 | {
418 | BatchItemRequest = new[]
419 | {
420 | new BatchItemRequest
421 | {
422 | bId = Guid.NewGuid().ToString(),
423 | Query = "SELECT * FROM Bill MAXRESULTS 30",
424 | },
425 | new BatchItemRequest
426 | {
427 | bId = Guid.NewGuid().ToString(),
428 | Query = "SELECT * FROM Invoice MAXRESULTS 30",
429 | }
430 | }
431 | });
432 | Assert.IsTrue(response.Response.ElementAt(0).QueryResponse.Bill.Length > 0);
433 | Assert.IsTrue(response.Response.ElementAt(1).QueryResponse.Invoice.Length > 0);
434 | }
435 |
436 | [TestMethod]
437 | public async Task GetInvoicePDFAsync()
438 | {
439 | var response = await _service.QueryAsync("SELECT * FROM Invoice MAXRESULTS 1");
440 |
441 | Assert.IsTrue(response.Response.Entities.Length > 0);
442 |
443 | var invoidePdfStream = await _service.GetInvoicePDFAsync(response.Response.Entities[0].Id);
444 |
445 | Assert.IsNotNull(invoidePdfStream);
446 | Assert.IsNotNull(invoidePdfStream.Length > 0);
447 | }
448 |
449 | [TestMethod]
450 | public async Task CreatePaymentAndVoidAsync()
451 | {
452 | // GET an Open Invoice
453 | var invoiceResponse = await _service.QueryAsync(
454 | "SELECT * FROM Invoice " +
455 | "WHERE Balance > '0' " +
456 | "ORDERBY DueDate DESC, Balance DESC " +
457 | "MAXRESULTS 1");
458 |
459 | if (invoiceResponse.Response.Entities.Length == 0)
460 | {
461 | Assert.Fail("No Invoices returned");
462 | }
463 |
464 | var invoice = invoiceResponse.Response.Entities[0];
465 |
466 | // Create a Payment for that Invoice
467 | var payment = new Payment()
468 | {
469 | CustomerRef = invoice.CustomerRef,
470 | TotalAmt = 1,
471 | Line = new[] {
472 | new Line
473 | {
474 | Amount = 1,
475 | LinkedTxn = new LinkedTxn[] { new LinkedTxn
476 | {
477 | TxnId = invoice.Id,
478 | TxnType = $"{QboEntityTypeEnum.INVOICE}"
479 | } }
480 | }
481 | },
482 | PrivateNote = "Payment made by QuickBooksSharp"
483 | };
484 |
485 | var paymentResponse = await _service.PostAsync(payment);
486 |
487 | Assert.IsTrue(paymentResponse.Response != null);
488 |
489 | // Void Payment
490 | var voidPayment = new Payment()
491 | {
492 | Id = paymentResponse.Response.Id,
493 | SyncToken = paymentResponse.Response.SyncToken,
494 | sparse = true,
495 | PrivateNote = "Payment voided by QuickBooksSharp"
496 | };
497 |
498 | var voidPaymentResponse = await _service.PostAsync(voidPayment, OperationEnum.update, OperationEnum.@void);
499 |
500 | Assert.IsNotNull(voidPaymentResponse.Response);
501 | }
502 |
503 | [TestMethod]
504 | public async Task CreateTaxCodeAndRateAsync()
505 | {
506 | var taxAgencyName = "Test Tax Agency";
507 | var taxAgency = await GetTaxAgencyAsync(taxAgencyName);
508 | taxAgency ??= await CreateTaxAgency(taxAgencyName);
509 |
510 | var guid = Guid.NewGuid();
511 | var taxCodeName = $"Test Tax Code {guid}";
512 | var taxRateName = $"Test Tax Rate {guid}";
513 | var taxRate = 10;
514 |
515 | var taxServiceResponse = await _service.PostTaxServiceAsync(new TaxService
516 | {
517 | TaxCode = taxCodeName,
518 | TaxRateDetails = new List
519 | {
520 | new()
521 | {
522 | RateValue = taxRate,
523 | TaxAgencyId = taxAgency.Id,
524 | TaxRateName = taxRateName
525 | }
526 | }.ToArray()
527 | });
528 |
529 | Assert.IsNotNull(taxServiceResponse.TaxCodeId);
530 | Assert.AreEqual(taxServiceResponse.TaxCode, taxCodeName);
531 |
532 | Assert.IsNotNull(taxServiceResponse.TaxRateDetails);
533 | Assert.AreEqual(taxServiceResponse.TaxRateDetails[0].TaxRateName, taxRateName);
534 | Assert.IsNotNull(taxServiceResponse.TaxRateDetails[0].TaxRateId);
535 | Assert.AreEqual(taxServiceResponse.TaxRateDetails[0].RateValue, taxRate);
536 | }
537 |
538 | private async Task GetTaxAgencyAsync(string taxAgencyName)
539 | {
540 | var result = await _service.QueryAsync("SELECT * FROM TaxAgency");
541 | var taxAgencies = result.Response.Entities.ToList();
542 | return taxAgencies.Find(ta => ta.DisplayName == taxAgencyName);
543 | }
544 |
545 | private async Task CreateTaxAgency(string taxAgencyName)
546 | {
547 | var result = await _service.PostAsync(new TaxAgency
548 | {
549 | DisplayName = taxAgencyName,
550 | Active = true
551 | });
552 | return result.Response;
553 | }
554 | }
555 | }
556 |
--------------------------------------------------------------------------------
/QuickBooksSharp.Tests/QuickBooksSharp.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/QuickBooksSharp.Tests/QuickBooksUrlTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 |
3 | namespace QuickBooksSharp.Tests
4 | {
5 | [TestClass]
6 | public class QuickBooksUrlTests
7 | {
8 | [TestMethod]
9 | public void ShouldCreateSandboxUrl()
10 | {
11 | var url = QuickBooksUrl.Build(true, 123);
12 |
13 | Assert.IsNotNull(url);
14 | Assert.AreEqual(url.ToUri().AbsoluteUri, $"https://sandbox-quickbooks.api.intuit.com/v3/company/123?minorversion={QuickBooksUrl.MinorVersion}");
15 |
16 | }
17 |
18 | [TestMethod]
19 | public void ShouldCreateProductionUrl()
20 | {
21 | var url = QuickBooksUrl.Build(false, 123);
22 |
23 | Assert.IsNotNull(url);
24 | Assert.AreEqual(url.ToUri().AbsoluteUri, $"https://quickbooks.api.intuit.com/v3/company/123?minorversion={QuickBooksUrl.MinorVersion}");
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/QuickBooksSharp.Tests/RunPolicyTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using QuickBooksSharp.Entities;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 |
6 | namespace QuickBooksSharp.Tests
7 | {
8 | [TestClass]
9 | [Ignore("Tests should be run manually")]
10 | public class RunPolicyTests : ServiceTestBase
11 | {
12 | private string _accessToken;
13 |
14 | [TestInitialize]
15 | public async Task Initialize()
16 | {
17 | _accessToken = await GetAccessTokenAsync();
18 | }
19 |
20 | private async Task IssueManyRequests(IRunPolicy policy)
21 | {
22 | var svc = new DataService(_accessToken, TestHelper.RealmId, true, policy);
23 | await Parallel.ForEachAsync(Enumerable.Range(1, 1000), new ParallelOptions { MaxDegreeOfParallelism = 50 }, async (i, token) =>
24 | {
25 | var res = await svc.QueryAsync("SELECT * FROM Customer WHERE Id = '1'");
26 | Assert.IsNotNull(res);
27 | Assert.IsNull(res.Fault);
28 | Assert.IsNotNull(res.Time);
29 | Assert.IsNotNull(res.Response);
30 | });
31 | }
32 |
33 | [TestMethod]
34 | public async Task NoRetryShouldFailWhenTooManyRequests()
35 | {
36 | QuickBooksException ex = null;
37 |
38 | try
39 | {
40 | await IssueManyRequests(new NoRetryRunPolicy());
41 | }
42 | catch (QuickBooksException e)
43 | {
44 | ex = e;
45 | }
46 |
47 | Assert.IsNotNull(ex);
48 | Assert.IsTrue(ex.IsRateLimit);
49 | }
50 |
51 | [TestMethod]
52 | public async Task RetryShouldSucceedWhenTooManyRequests()
53 | {
54 | QuickBooksException ex = null;
55 |
56 | try
57 | {
58 | await IssueManyRequests(new SimpleRetryRunPolicy());
59 | }
60 | catch (QuickBooksException e)
61 | {
62 | ex = e;
63 | }
64 |
65 | Assert.IsNull(ex);
66 | }
67 |
68 | [TestMethod]
69 | public async Task MaxConccurencyRetryShouldSucceedWhenTooManyRequests()
70 | {
71 | QuickBooksException ex = null;
72 |
73 | try
74 | {
75 | await IssueManyRequests(new MaxConcurrencyRetryRunPolicy());
76 | }
77 | catch (QuickBooksException e)
78 | {
79 | ex = e;
80 | }
81 |
82 | Assert.IsNull(ex);
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/QuickBooksSharp.Tests/ServiceTestBase.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace QuickBooksSharp.Tests
4 | {
5 | public abstract class ServiceTestBase
6 | {
7 | protected async Task GetAccessTokenAsync()
8 | {
9 | var res = await new AuthenticationService().RefreshOAuthTokenAsync(TestHelper.ClientId, TestHelper.ClientSecret, TestHelper.RefreshToken);
10 | if (res.refresh_token != TestHelper.RefreshToken)
11 | TestHelper.PersistNewRefreshToken(res.refresh_token);
12 | return res.access_token;
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/QuickBooksSharp.Tests/TestHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace QuickBooksSharp.Tests
4 | {
5 | ///
6 | /// https://developer.intuit.com/app/developer/playground makes it easy to generate values from the variables below
7 | ///
8 | public class TestHelper
9 | {
10 | ///
11 | /// Used to run AuthenticationService tests
12 | ///
13 | public static string ClientId = GetEnvVar("QUICKBOOKS_SHARP_CLIENT_ID");
14 |
15 | public static string ClientSecret = GetEnvVar("QUICKBOOKS_SHARP_CLIENT_SECRET");
16 |
17 | public static string RedirectUri = GetEnvVar("QUICKBOOKS_SHARP_OAUTH_REDIRECT_URI");
18 |
19 | public static long RealmId = long.Parse(GetEnvVar("QUICKBOOKS_SHARP_REALMID"));
20 |
21 | public static string RefreshToken = GetEnvVar("QUICKBOOKS_SHARP_REFRESH_TOKEN");
22 |
23 | public static void PersistNewRefreshToken(string refreshToken) => Environment.SetEnvironmentVariable("QUICKBOOKS_SHARP_REFRESH_TOKEN", refreshToken, EnvironmentVariableTarget.User);
24 |
25 | private static string GetEnvVar(string name) =>
26 | Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.User)
27 | ?? Environment.GetEnvironmentVariable(name)
28 | ?? throw new Exception($"Environment {name} is not defined");
29 |
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/QuickBooksSharp.Tests/WebhookEventTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using System;
3 | using System.Text.Json;
4 |
5 | namespace QuickBooksSharp.Tests
6 | {
7 | [TestClass]
8 | public class WebhookEventTests
9 | {
10 | private readonly string _validNotification =
11 | @"{
12 | ""eventNotifications"":[
13 | {
14 | ""realmId"":""4620816365179422850"",
15 | ""dataChangeEvent"": {
16 | ""entities"": [
17 | {
18 | ""name"":""Customer"",
19 | ""id"":""2"",
20 | ""operation"":""Update"",
21 | ""lastUpdated"":""2021-09-28T15:27:59.000Z""
22 | },
23 | {
24 | ""name"":""Invoice"",
25 | ""id"":""3"",
26 | ""operation"":""Void"",
27 | ""lastUpdated"":""2021-10-28T15:27:59.000Z""
28 | },
29 | {
30 | ""name"":""CreditMemo"",
31 | ""id"":""4"",
32 | ""operation"":""merge"",
33 | ""lastUpdated"":""2021-11-28T15:27:59.000Z"",
34 | ""deletedId"":""5""
35 | }
36 | ]
37 | }
38 | }
39 | ]
40 | }";
41 |
42 | [TestMethod]
43 | public void ShouldDeserializeValidJson()
44 | {
45 | WebhookEvent notification = JsonSerializer.Deserialize(_validNotification, QuickBooksHttpClient.JsonSerializerOptions);
46 |
47 | Assert.IsNotNull(notification);
48 | Assert.IsNotNull(notification.EventNotifications);
49 | Assert.IsTrue(notification.EventNotifications.Length > 0);
50 | Assert.IsNotNull(notification.EventNotifications[0]);
51 | Assert.IsNotNull(notification.EventNotifications[0].DataChangeEvent);
52 | Assert.IsNotNull(notification.EventNotifications[0].DataChangeEvent.Entities);
53 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities.Length > 0);
54 | // Customer
55 | Assert.IsNotNull(notification.EventNotifications[0].DataChangeEvent.Entities[0]);
56 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[0].Id == "2");
57 | Assert.IsNull(notification.EventNotifications[0].DataChangeEvent.Entities[0].DeletedId);
58 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[0].Name == EntityChangedName.Customer);
59 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[0].Operation == Entities.OperationEnum.update);
60 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[0].LastUpdated == new DateTime(2021, 9, 28, 15, 27, 59, DateTimeKind.Utc));
61 | // Invoice
62 | Assert.IsNotNull(notification.EventNotifications[0].DataChangeEvent.Entities[1]);
63 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[1].Id == "3");
64 | Assert.IsNull(notification.EventNotifications[0].DataChangeEvent.Entities[1].DeletedId);
65 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[1].Name == EntityChangedName.Invoice);
66 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[1].Operation == Entities.OperationEnum.@void);
67 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[1].LastUpdated == new DateTime(2021, 10, 28, 15, 27, 59, DateTimeKind.Utc));
68 | // CreditMemo
69 | Assert.IsNotNull(notification.EventNotifications[0].DataChangeEvent.Entities[2]);
70 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[2].Id == "4");
71 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[2].DeletedId == "5");
72 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[2].Name == EntityChangedName.CreditMemo);
73 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[2].Operation == Entities.OperationEnum.merge);
74 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[2].LastUpdated == new DateTime(2021, 11, 28, 15, 27, 59, DateTimeKind.Utc));
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/QuickBooksSharp.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.31129.286
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuickBooksSharp", "QuickBooksSharp\QuickBooksSharp.csproj", "{94291700-D916-46F5-A881-9B532241B466}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuickBooksSharp.Tests", "QuickBooksSharp.Tests\QuickBooksSharp.Tests.csproj", "{1FB4D237-ED32-42FF-93CB-5AEE976A08A9}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Misc", "Misc", "{E347FD34-F1C5-4511-ABC5-3DD19D44A6B1}"
11 | ProjectSection(SolutionItems) = preProject
12 | .github\workflows\ci.yml = .github\workflows\ci.yml
13 | LICENSE = LICENSE
14 | README.md = README.md
15 | EndProjectSection
16 | EndProject
17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickBooksSharp.CodeGen", "QuickBooksSharp.CodeGen\QuickBooksSharp.CodeGen.csproj", "{2941A980-49D2-4BA7-8A74-000C298A9497}"
18 | EndProject
19 | Global
20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
21 | Debug|Any CPU = Debug|Any CPU
22 | Release|Any CPU = Release|Any CPU
23 | EndGlobalSection
24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
25 | {94291700-D916-46F5-A881-9B532241B466}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26 | {94291700-D916-46F5-A881-9B532241B466}.Debug|Any CPU.Build.0 = Debug|Any CPU
27 | {94291700-D916-46F5-A881-9B532241B466}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {94291700-D916-46F5-A881-9B532241B466}.Release|Any CPU.Build.0 = Release|Any CPU
29 | {1FB4D237-ED32-42FF-93CB-5AEE976A08A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
30 | {1FB4D237-ED32-42FF-93CB-5AEE976A08A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
31 | {1FB4D237-ED32-42FF-93CB-5AEE976A08A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
32 | {1FB4D237-ED32-42FF-93CB-5AEE976A08A9}.Release|Any CPU.Build.0 = Release|Any CPU
33 | {2941A980-49D2-4BA7-8A74-000C298A9497}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
34 | {2941A980-49D2-4BA7-8A74-000C298A9497}.Debug|Any CPU.Build.0 = Debug|Any CPU
35 | {2941A980-49D2-4BA7-8A74-000C298A9497}.Release|Any CPU.ActiveCfg = Release|Any CPU
36 | {2941A980-49D2-4BA7-8A74-000C298A9497}.Release|Any CPU.Build.0 = Release|Any CPU
37 | EndGlobalSection
38 | GlobalSection(SolutionProperties) = preSolution
39 | HideSolutionNode = FALSE
40 | EndGlobalSection
41 | GlobalSection(ExtensibilityGlobals) = postSolution
42 | SolutionGuid = {B8CDFCEB-F9EC-4C3E-9780-6915314200F5}
43 | EndGlobalSection
44 | EndGlobal
45 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Authentication/AuthenticationService.cs:
--------------------------------------------------------------------------------
1 | using Flurl;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Net.Http;
5 | using System.Text;
6 | using System.Text.Json;
7 | using System.Threading.Tasks;
8 |
9 | namespace QuickBooksSharp
10 | {
11 | public class AuthenticationService : IAuthenticationService
12 | {
13 | private readonly QuickBooksHttpClient _client = new QuickBooksHttpClient(null, null, new NoRetryRunPolicy());
14 |
15 | //TODO: retrieve the endpoints URLs dynamically
16 | //See https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-openid-discovery-doc
17 |
18 | private const string TOKEN_ENDPOINT_URL = "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer";
19 | private const string REVOKE_TOKEN_ENDPOINT_URL = "https://developer.api.intuit.com/v2/oauth2/tokens/revoke";
20 | private const string USER_INFO_ENDPOINT_URL = "https://accounts.platform.intuit.com/v1/openid_connect/userinfo";
21 | private const string USER_INFO_ENDPOINT_SANDBOX_URL = "https://sandbox-accounts.platform.intuit.com/v1/openid_connect/userinfo";
22 |
23 | public string GenerateAuthorizationPromptUrl(string clientId, IEnumerable scopes, string redirectUrl, string state)
24 | {
25 | return new Url("https://appcenter.intuit.com/connect/oauth2")
26 | .SetQueryParam("client_id", clientId)
27 | .SetQueryParam("scope", string.Join(" ", scopes))
28 | .SetQueryParam("redirect_uri", redirectUrl)
29 | .SetQueryParam("response_type", "code")
30 | .SetQueryParam("state", state)
31 | .ToString();
32 | }
33 |
34 | public async Task GetUserInfo(string accessToken, bool useSandbox)
35 | {
36 | return await new QuickBooksHttpClient(accessToken, null, RunPolicy.DefaultRunPolicy).GetAsync(useSandbox ? USER_INFO_ENDPOINT_SANDBOX_URL : USER_INFO_ENDPOINT_URL);
37 | }
38 |
39 | public async Task GetOAuthTokenAsync(string clientId, string clientSecret, string code, string redirectUrl)
40 | {
41 | return await _client.SendAsync(() =>
42 | {
43 | var request = new HttpRequestMessage(HttpMethod.Post, TOKEN_ENDPOINT_URL)
44 | {
45 | #pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
46 | Content = new FormUrlEncodedContent(new Dictionary
47 | {
48 | { "code", code },
49 | { "redirect_uri", redirectUrl },
50 | { "grant_type", "authorization_code" },
51 | })
52 | #pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
53 | };
54 | this.AddAuthenticationHeader(request, clientId, clientSecret);
55 | return request;
56 | });
57 | }
58 |
59 | ///
60 | /// When refreshed, the previous access token is invalidated immediately
61 | ///
62 | ///
63 | public async Task RefreshOAuthTokenAsync(string clientId, string clientSecret, string refreshToken)
64 | {
65 | return await _client.SendAsync(() =>
66 | {
67 | var request = new HttpRequestMessage(HttpMethod.Post, TOKEN_ENDPOINT_URL)
68 | {
69 | #pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
70 | Content = new FormUrlEncodedContent(new Dictionary
71 | {
72 | { "refresh_token", refreshToken },
73 | { "grant_type", "refresh_token" },
74 | })
75 | #pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
76 | };
77 | this.AddAuthenticationHeader(request, clientId, clientSecret);
78 | return request;
79 | });
80 | }
81 |
82 | public async Task RevokeOAuthTokenAsync(string clientId, string clientSecret, string tokenOrRefreshToken)
83 | {
84 | await _client.SendAsync(() =>
85 | {
86 | var request = new HttpRequestMessage(HttpMethod.Post, REVOKE_TOKEN_ENDPOINT_URL)
87 | {
88 | Content = new StringContent(JsonSerializer.Serialize(new { token = tokenOrRefreshToken }), Encoding.UTF8, "application/json")
89 | };
90 | this.AddAuthenticationHeader(request, clientId, clientSecret);
91 | return request;
92 | });
93 | }
94 |
95 | private void AddAuthenticationHeader(HttpRequestMessage request, string clientId, string clientSecret)
96 | {
97 | request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{clientId}:{clientSecret}")));
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Authentication/IAuthenticationService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 |
4 | namespace QuickBooksSharp
5 | {
6 | public interface IAuthenticationService
7 | {
8 | ///
9 | /// Create the authorization request your app will send to the Intuit OAuth 2.0 Server.
10 | /// Request parameters should identify your app and include the required scopes.
11 | /// https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-2.0#authorization-request
12 | ///
13 | /// Identifies the app making the request.
14 | /// Lists the scopes your app uses. The list is space-delimited.
15 | /// Enter one or more scopes.The scope value defines the apps’ access and type of data it can utilize.This information appears on the authorization page when end-users connect to your app.
16 | /// Tip: We recommend apps request scopes incrementally based on your feature requirements, rather than every scope up front.
17 | /// Determines where the Intuit OAuth 2.0 Server redirects users after they grant permission to your app.
18 | /// The redirect value must match the URI you listed in Step 6 exactly.That includes the casing, http scheme, and trailing “/.”
19 | /// Defines the state between your authorization request and the Intuit OAuth 2.0 Server response.
20 | /// The purpose of the state field is to validate if the client (i.e.your app) gets back what was sent in the original request.Thus, the state is maintained from send to response.
21 | /// You can enter any string value to maintain the state. The server should return the exact name:value pair you sent in the original request.
22 | /// Tip: We strongly recommend you include an anti-forgery token for the state and confirm it in the response This prevents cross-site request forgery.Learn more about CSRF.
23 | /// Url for authorization request
24 | string GenerateAuthorizationPromptUrl(string clientId, IEnumerable scopes, string redirectUrl, string state);
25 | ///
26 | /// At this point, your app is waiting for a response from the Intuit OAuth 2.0 Server. If users approve access, the Intuit OAuth 2.0 Server sends a response to the redirect URI you specified.
27 | ///
28 | /// The client id of your QuickBooks application making the request
29 | /// The client secret of your QuickBooks application
30 | /// The authorization code sent by the Intuit OAuth 2.0 Server. Max length: 512 characters
31 | /// Determines where the Intuit OAuth 2.0 Server redirects users after they grant permission to your app.
32 | /// The redirect value must match the URI you listed in Step 6 exactly.That includes the casing, http scheme, and trailing “/.”
33 | ///
34 | Task GetOAuthTokenAsync(string clientId, string clientSecret, string code, string redirectUrl);
35 | ///
36 | /// Obtains User information
37 | ///
38 | /// The token used to access our API. Max length: 4096 characters.
39 | /// Determines if Sandbox environment is used
40 | ///
41 | Task GetUserInfo(string accessToken, bool useSandbox);
42 | ///
43 | /// Access tokens eventually expire. Use refresh tokens to “refresh” expired access tokens. You can refresh access tokens without prompting users for permission.
44 | ///
45 | /// The client id of your QuickBooks application making the request
46 | /// The client secret of your QuickBooks application
47 | /// The last refresh token obtained from either or
48 | ///
49 | Task RefreshOAuthTokenAsync(string clientId, string clientSecret, string refreshToken);
50 | ///
51 | /// If users need to disconnect from your app, you need a way to automatically revoke access. You can use the sample code below as a model.
52 | ///
53 | /// The client id of your QuickBooks application making the request
54 | /// The client secret of your QuickBooks application
55 | /// The last access or refresh token obtained from either or
56 | ///
57 | Task RevokeOAuthTokenAsync(string clientId, string clientSecret, string tokenOrRefreshToken);
58 | }
59 | }
--------------------------------------------------------------------------------
/QuickBooksSharp/Authentication/RevokeTokenRequest.cs:
--------------------------------------------------------------------------------
1 | namespace QuickBooksSharp
2 | {
3 | public class RevokeTokenRequest
4 | {
5 | ///
6 | /// token or refresh_token
7 | ///
8 | public string token { get; set; } = default!;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Authentication/TokenRequest.cs:
--------------------------------------------------------------------------------
1 | namespace QuickBooksSharp
2 | {
3 | public class TokenRequest
4 | {
5 | public string code { get; set; } = default!;
6 |
7 | public string redirect_uri { get; set; } = default!;
8 |
9 | ///
10 | /// "authorization_code" or "refresh_token"
11 | ///
12 | public string grant_type { get; set; } = default!;
13 |
14 | public string refresh_token { get; set; } = default!;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Authentication/TokenResponse.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using System.Text.Json.Serialization;
5 |
6 | namespace QuickBooksSharp
7 | {
8 | public class TokenResponse
9 | {
10 | ///
11 | /// "bearer"
12 | ///
13 | public string token_type { get; set; } = default!;
14 |
15 | public string access_token { get; set; } = default!;
16 |
17 | public string refresh_token { get; set; } = default!;
18 |
19 | ///
20 | /// duration in seconds
21 | ///
22 | [JsonConverter(typeof(NumberTimespanConverter))]
23 | public TimeSpan expires_in { get; set; }
24 |
25 | ///
26 | /// duration in seconds
27 | ///
28 | [JsonConverter(typeof(NumberTimespanConverter))]
29 | public TimeSpan x_refresh_token_expires_in { get; set; }
30 |
31 | public string? id_token { get; set; }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Authentication/UserInfo.cs:
--------------------------------------------------------------------------------
1 | namespace QuickBooksSharp
2 | {
3 | public class UserInfo
4 | {
5 | #pragma warning disable CS8618
6 | public string sub { get; set; }
7 | #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
8 |
9 | public string? email { get; set; }
10 |
11 | public bool? emailVerified { get; set; }
12 |
13 | public string? givenName { get; set; }
14 |
15 | public string? familyName { get; set; }
16 |
17 | public string? phoneNumber { get; set; }
18 |
19 | public bool? phoneNumberVerified { get; set; }
20 |
21 | public UserAddress? address { get; set; }
22 |
23 | public class UserAddress
24 | {
25 | public string? streetAddress { get; set; }
26 |
27 | public string? locality { get; set; }
28 |
29 | public string? region { get; set; }
30 |
31 | public string? postalCode { get; set; }
32 |
33 | public string? country { get; set; }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Entities/IntuitResponseOfT.cs:
--------------------------------------------------------------------------------
1 | using QuickBooksSharp.Entities;
2 | using System;
3 |
4 | namespace QuickBooksSharp
5 | {
6 | public class IntuitResponse where TResponse : class
7 | {
8 | ///
9 | /// Indication that a request was processed, but with possible exceptional circumstances (i.e. ignored unsupported fields) that the client may want to be aware of
10 | ///
11 | public Warnings? Warnings { get; set; }
12 |
13 | ///
14 | /// RequestId associated with the request
15 | ///
16 | public string? RequestId { get; set; }
17 |
18 | ///
19 | /// Time at which request started processing in the server
20 | ///
21 | public DateTimeOffset? Time { get; set; }
22 |
23 | ///
24 | /// HTTP codes result of the operation<
25 | ///
26 | public string? Status { get; set; }
27 |
28 | public Fault? Fault { get; set; }
29 |
30 | public TResponse? Response { get; set; }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Entities/QueryCountResponse.cs:
--------------------------------------------------------------------------------
1 | using QuickBooksSharp.Entities;
2 |
3 | namespace QuickBooksSharp
4 | {
5 | public class QueryCountResponse
6 | {
7 | public Warnings? Warnings { get; set; }
8 |
9 | public int? TotalCount { get; set; }
10 |
11 | public Fault? Fault { get; set; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Entities/QueryResponseOfT.cs:
--------------------------------------------------------------------------------
1 | using QuickBooksSharp.Entities;
2 |
3 | namespace QuickBooksSharp
4 | {
5 | public class QueryResponse where TEntity : IntuitEntity
6 | {
7 | public Warnings? Warnings { get; set; }
8 |
9 | public int? StartPosition { get; set; }
10 |
11 | public int? MaxResults { get; set; }
12 |
13 | public int? TotalCount { get; set; }
14 |
15 | public Fault? Fault { get; set; }
16 |
17 | public TEntity[]? Entities { get; set; }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Helper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Security.Cryptography;
3 | using System.Text;
4 | using System.Text.Json;
5 |
6 | namespace QuickBooksSharp
7 | {
8 | public class Helper
9 | {
10 | ///
11 | /// Returns whether the webhook request was signed by Intui
12 | ///
13 | /// Value from the HTTP header "intuit-signature"
14 | /// The webhook verifier token located in the Intuit Developer dashboard
15 | /// The full HTTP requests body
16 | ///
17 | public static bool IsAuthenticWebhook(string intuitSignature, string webhookVerifierToken, string requestBody)
18 | {
19 | var verifierTokenBytes = Encoding.UTF8.GetBytes(webhookVerifierToken);
20 | var jsonBytes = Encoding.UTF8.GetBytes(requestBody);
21 | var hmac = new HMACSHA256(verifierTokenBytes);
22 | var hmacBytes = hmac.ComputeHash(jsonBytes);
23 | var hash = Convert.ToBase64String(hmacBytes);
24 | return hash == intuitSignature;
25 | }
26 |
27 | public static string SerializeToJSON(object o)
28 | {
29 | return JsonSerializer.Serialize(o, QuickBooksHttpClient.JsonSerializerOptions);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Infrastructure/IQuickBooksHttpClient.cs:
--------------------------------------------------------------------------------
1 | using Flurl;
2 | using System;
3 | using System.Net.Http;
4 | using System.Threading.Tasks;
5 |
6 | namespace QuickBooksSharp
7 | {
8 | public interface IQuickBooksHttpClient
9 | {
10 | Task GetAsync(Url url);
11 | Task PostAsync(Url url, object content);
12 | Task SendAsync(Func makeRequest);
13 | Task SendAsync(Func makeRequest);
14 | }
15 | }
--------------------------------------------------------------------------------
/QuickBooksSharp/Infrastructure/NumberTimespanConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 |
5 | namespace QuickBooksSharp
6 | {
7 | public class NumberTimespanConverter : JsonConverter
8 | {
9 | public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
10 | {
11 | return TimeSpan.FromSeconds(reader.GetInt32());
12 | }
13 |
14 | public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
15 | {
16 | writer.WriteNumberValue((int)value.TotalSeconds);
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Infrastructure/QuickBooksException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Net.Http;
4 |
5 | namespace QuickBooksSharp
6 | {
7 | public class QuickBooksException : Exception
8 | {
9 | public HttpRequestMessage Request { get; }
10 |
11 | public HttpResponseMessage Response { get; }
12 |
13 | public string ResponseContent { get; }
14 |
15 | ///
16 | /// HTTP 401
17 | ///
18 | public bool IsUnauthorized => (int)Response.StatusCode == 401;
19 |
20 | ///
21 | /// HTTP 403
22 | ///
23 | public bool IsForbidden => (int)Response.StatusCode == 403;
24 |
25 | ///
26 | /// HTTP 429
27 | ///
28 | public bool IsRateLimit => (int)Response.StatusCode == 429;
29 |
30 | public string? IntuitTId => GetHeaderValue(Response, "intuit_tid");
31 |
32 | public string? QBOVersion => GetHeaderValue(Response, "QBO-Version");
33 |
34 | public string? ErrorCode => GetHeaderValue(Response, "ErrorCode");
35 |
36 | public string? ErrorCause => GetHeaderValue(Response, "ErrorCause");
37 |
38 | private static string? GetHeaderValue(HttpResponseMessage r, string headerName) => r.Headers.TryGetValues(headerName, out var values) ? values.FirstOrDefault() : null;
39 |
40 | public QuickBooksException(HttpRequestMessage request, HttpResponseMessage response, string responseContent)
41 | : base($@"QuickBooks API call to {request.RequestUri} failed with code: {response.StatusCode}
42 | IntuiTId: {GetHeaderValue(response, "intuit_tid")},
43 | Reason: {response.ReasonPhrase}
44 | Content: {responseContent}")
45 | {
46 | this.Request = request;
47 | this.Response = response;
48 | this.ResponseContent = responseContent;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Infrastructure/QuickBooksHttpClient.cs:
--------------------------------------------------------------------------------
1 | using Flurl;
2 | using System;
3 | using System.Net;
4 | using System.Net.Http;
5 | using System.Net.Http.Headers;
6 | using System.Net.Http.Json;
7 | using System.Text;
8 | using System.Text.Json;
9 | using System.Text.Json.Serialization;
10 | using System.Threading.Tasks;
11 |
12 | namespace QuickBooksSharp
13 | {
14 | public class QuickBooksHttpClient : IQuickBooksHttpClient
15 | {
16 | private readonly string? _accessToken;
17 | private readonly long? _realmId;
18 | private IRunPolicy _runPolicy;
19 |
20 | private static HttpClient _httpClient = new HttpClient(new HttpClientHandler
21 | {
22 | AutomaticDecompression = DecompressionMethods.GZip
23 | });
24 |
25 |
26 | public readonly static JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions
27 | {
28 | Converters =
29 | {
30 | //new JsonStringEnumConverter(),
31 |
32 | //using community package to fix https://github.com/dotnet/runtime/issues/31081
33 | //can revert to out of the box converter once fix (.net 6?)
34 | new JsonStringEnumMemberConverter()
35 | }
36 | };
37 |
38 | static QuickBooksHttpClient()
39 | {
40 | _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(nameof(QuickBooksSharp), typeof(QuickBooksHttpClient).Assembly.GetName().Version!.ToString()));
41 | _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("(github.com/better-reports/QuickBooksSharp)"));
42 | _httpClient.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
43 | _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
44 | }
45 |
46 | public QuickBooksHttpClient(string? accessToken, long? realmId, IRunPolicy runPolicy)
47 | {
48 | _accessToken = accessToken;
49 | _realmId = realmId;
50 | _runPolicy = runPolicy;
51 | }
52 |
53 | public async Task GetAsync(Url url)
54 | {
55 | Func makeRequest = () => new HttpRequestMessage(HttpMethod.Get, url);
56 | return await this.SendAsync(makeRequest);
57 | }
58 |
59 | public async Task PostAsync(Url url, object content)
60 | {
61 | Func makeRequest = () => new HttpRequestMessage(HttpMethod.Post, url)
62 | {
63 | Content = new StringContent(JsonSerializer.Serialize(content, JsonSerializerOptions), Encoding.UTF8, "application/json")
64 | };
65 | return await this.SendAsync(makeRequest);
66 | }
67 |
68 | public async Task SendAsync(Func makeRequest)
69 | {
70 | var response = await this.SendAsync(makeRequest);
71 | return (await response.Content.ReadFromJsonAsync(JsonSerializerOptions))!;
72 | }
73 |
74 | public async Task SendAsync(Func makeRequest)
75 | {
76 | var response = await this._runPolicy.RunAsync(_realmId, async () =>
77 | {
78 | using (var request = makeRequest())
79 | {
80 | if (_accessToken != null)
81 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
82 |
83 | var response = await _httpClient.SendAsync(request);
84 | var ex = response.IsSuccessStatusCode ? null : new QuickBooksException(request, response, await response.Content.ReadAsStringAsync());
85 |
86 | if (ex?.IsRateLimit == true)
87 | RunPolicy.NotifyRateLimt(new RateLimitEvent(_realmId, request.RequestUri));
88 |
89 | return new QuickBooksAPIResponse(response, ex);
90 | }
91 | });
92 |
93 | return response;
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Infrastructure/QuickBooksUrl.cs:
--------------------------------------------------------------------------------
1 | using Flurl;
2 |
3 | namespace QuickBooksSharp
4 | {
5 | ///
6 | /// Creates the BaseUrl for QuickBooks Online API
7 | ///
8 | public static class QuickBooksUrl
9 | {
10 | public const string SandboxBaseUrl = "https://sandbox-quickbooks.api.intuit.com";
11 | public const string ProductionBaseUrl = "https://quickbooks.api.intuit.com";
12 |
13 | public const int MajorVersion = 3;
14 |
15 | public const int MinorVersion = 75;
16 |
17 | public static readonly string Version = $"{MajorVersion}.{MinorVersion}";
18 |
19 | ///
20 | /// Creates the using the and specified.
21 | ///
22 | /// Determines if the Sandbox or Production url will be used.
23 | /// The realm ID is a unique ID value which identifies a specific QuickBooks Online company
24 | ///
25 | /// Creates a instance with:
26 | /// Sandbox: $"{}/v3/company/{}?minorversion=}"
27 | /// Production: $"{}/v3/company/{}?minorversion=}"
28 | ///
29 | public static Url Build(bool useSandbox, long realmId)
30 | {
31 | var serviceBaseUrl = useSandbox ? SandboxBaseUrl : ProductionBaseUrl;
32 |
33 | return new Url($"{serviceBaseUrl}/v{MajorVersion}/company/{realmId}")
34 | .SetQueryParam("minorversion", MinorVersion);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Policies/FifoSemaphore.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Concurrent;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 |
5 | namespace QuickBooksSharp
6 | {
7 | internal class FifoSemaphore
8 | {
9 | private SemaphoreSlim _semaphore;
10 |
11 | private ConcurrentQueue> queue = new ConcurrentQueue>();
12 |
13 | public int QueueCount => queue.Count;
14 |
15 | public FifoSemaphore(int maxConcurrency)
16 | {
17 | _semaphore = new SemaphoreSlim(maxConcurrency, maxConcurrency);
18 | }
19 |
20 | public Task WaitAsync()
21 | {
22 | var tcs = new TaskCompletionSource();
23 | queue.Enqueue(tcs);
24 | _semaphore.WaitAsync().ContinueWith(t =>
25 | {
26 | if (queue.TryDequeue(out var popped))
27 | popped.SetResult(true);
28 | });
29 | return tcs.Task;
30 | }
31 |
32 | public void Release()
33 | {
34 | _semaphore.Release();
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Policies/IRunPolicy.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using System.Threading.Tasks;
4 |
5 | namespace QuickBooksSharp
6 | {
7 | public interface IRunPolicy
8 | {
9 | //we use a request factory to make a new request when a retry is needed
10 | //that is because request message cannot be reused
11 | Task RunAsync(long? realmId, Func> getResponseAsync);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Policies/MaxConcurrencyRetryRunPolicy.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Net.Http;
4 | using System.Threading.Tasks;
5 |
6 | namespace QuickBooksSharp
7 | {
8 | public class MaxConcurrencyRetryRunPolicy : IRunPolicy
9 | {
10 | //QBO API allows up to 10 concurrent requests
11 | //https://developer.intuit.com/app/developer/qbo/docs/learn/rest-api-features#limits-and-throttles
12 | private const int MAX_CONCURRENT_REQUESTS = 10;
13 |
14 | private ConcurrentDictionary _realmIdToQueue = new ConcurrentDictionary();
15 |
16 | public int GetQueueCount(long realmId) => _realmIdToQueue[realmId].QueueCount;
17 |
18 | public async Task RunAsync(long? realmId, Func> getResponseAsync)
19 | {
20 | FifoSemaphore? sem = null;
21 |
22 | if (realmId != null)
23 | {
24 | sem = _realmIdToQueue.GetOrAdd(realmId.Value, _ => new FifoSemaphore(MAX_CONCURRENT_REQUESTS));
25 | await sem.WaitAsync();
26 | }
27 |
28 | try
29 | {
30 | return await RunCoreAsync(getResponseAsync);
31 | }
32 | finally
33 | {
34 | sem?.Release();
35 | }
36 | }
37 |
38 | private async Task RunCoreAsync(Func> getResponseAsync)
39 | {
40 | while (true)
41 | {
42 | var r = await getResponseAsync();
43 |
44 | if (r.Exception?.IsRateLimit == true)
45 | {
46 | await Task.Delay(TimeSpan.FromSeconds(5));
47 | continue;
48 | }
49 |
50 | if (r.Exception != null)
51 | throw r.Exception;
52 |
53 | return r.Response;
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Policies/NoRetryRunPolicy.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using System.Threading.Tasks;
4 |
5 | namespace QuickBooksSharp
6 | {
7 | public class NoRetryRunPolicy : IRunPolicy
8 | {
9 | public async Task RunAsync(long? realmId, Func> getResponseAsync)
10 | {
11 | var r = await getResponseAsync();
12 |
13 | if (r.Exception != null)
14 | throw r.Exception;
15 |
16 | return r.Response;
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Policies/QuickBooksAPIResponse.cs:
--------------------------------------------------------------------------------
1 | using System.Net.Http;
2 |
3 | namespace QuickBooksSharp
4 | {
5 | public class QuickBooksAPIResponse
6 | {
7 | internal HttpResponseMessage Response { get; private set; }
8 |
9 | internal QuickBooksException? Exception { get; private set; }
10 |
11 | public QuickBooksAPIResponse(HttpResponseMessage response, QuickBooksException? ex)
12 | {
13 | Response = response;
14 | Exception = ex;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Policies/RateLimitEvent.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace QuickBooksSharp
4 | {
5 | public class RateLimitEvent
6 | {
7 | public long? RealmId { get; }
8 |
9 | public Uri RequestUri { get; }
10 |
11 | public RateLimitEvent(long? realmId, Uri requestUri)
12 | {
13 | RealmId = realmId;
14 | RequestUri = requestUri;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Policies/RunPolicy.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reactive.Linq;
3 | using System.Reactive.Subjects;
4 |
5 | namespace QuickBooksSharp
6 | {
7 | public static class RunPolicy
8 | {
9 | public static IRunPolicy DefaultRunPolicy { get; set; } = new MaxConcurrencyRetryRunPolicy();
10 |
11 | internal static ISubject _RateLimitFired = new Subject();
12 |
13 | public static readonly IObservable RateLimitFired = _RateLimitFired.AsObservable();
14 |
15 | internal static void NotifyRateLimt(RateLimitEvent rateLimitEvent)
16 | {
17 | _RateLimitFired.OnNext(rateLimitEvent);
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Policies/SimpleRetryRunPolicy.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using System.Threading.Tasks;
4 |
5 | namespace QuickBooksSharp
6 | {
7 | public class SimpleRetryRunPolicy : IRunPolicy
8 | {
9 | public async Task RunAsync(long? realmId, Func> getResponseAsync)
10 | {
11 | while (true)
12 | {
13 | var r = await getResponseAsync();
14 |
15 | if (r.Exception?.IsRateLimit == true)
16 | {
17 | await Task.Delay(TimeSpan.FromSeconds(5));
18 | continue;
19 | }
20 |
21 | if (r.Exception != null)
22 | throw r.Exception;
23 |
24 | return r.Response;
25 | }
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/QuickBooksSharp/QuickBooksSharp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0;net8.0;net9.0
5 | Better Reports
6 | Better Reports
7 |
8 | true
9 | https://github.com/better-reports/QuickBooksSharp
10 | https://github.com/better-reports/QuickBooksSharp
11 | Modern .NET Client for QuickBooks Online API
12 | 3.5.0
13 | enable
14 | quickbooks online intuit qbo accounting
15 | DateOnly properties generated for .NET6+
16 | latest
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Services/DataService.cs:
--------------------------------------------------------------------------------
1 | using Flurl;
2 | using QuickBooksSharp.Entities;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Net.Http;
8 | using System.Net.Http.Headers;
9 | using System.Threading.Tasks;
10 |
11 | namespace QuickBooksSharp
12 | {
13 | public class DataService : IDataService
14 | {
15 | protected readonly QuickBooksHttpClient _client;
16 |
17 | protected readonly Url _serviceUrl;
18 |
19 | public DataService(string accessToken, long realmId, bool useSandbox, IRunPolicy? runPolicy = null)
20 | {
21 | _client = new QuickBooksHttpClient(accessToken, realmId, runPolicy ?? RunPolicy.DefaultRunPolicy);
22 | _serviceUrl = QuickBooksUrl.Build(useSandbox, realmId);
23 | }
24 |
25 | public async Task> QueryCountAsync(string queryCount)
26 | {
27 | var res = await QueryAsync(queryCount);
28 | return new IntuitResponse
29 | {
30 | RequestId = res.RequestId,
31 | Time = res.Time,
32 | Status = res.Status,
33 | Warnings = res.Warnings,
34 | Fault = res.Fault,
35 | Response = new QueryCountResponse
36 | {
37 | Fault = res.Response?.Fault,
38 | Warnings = res.Response?.Warnings,
39 | TotalCount = res.Response?.TotalCount
40 | }
41 | };
42 | }
43 |
44 | public async Task>> QueryAsync(string query) where TEntity : IntuitEntity
45 | {
46 | var res = await _client.GetAsync(new Url(_serviceUrl).AppendPathSegment("query")
47 | .SetQueryParam("query", query));
48 | var queryRes = res.QueryResponse;
49 | return new IntuitResponse>
50 | {
51 | RequestId = res.requestId,
52 | Time = res.time,
53 | Status = res.status,
54 | Warnings = res.Warnings,
55 | Fault = res.Fault,
56 | Response = new QueryResponse
57 | {
58 | MaxResults = queryRes?.maxResults,
59 | StartPosition = queryRes?.startPosition,
60 | TotalCount = queryRes?.totalCount,
61 | Warnings = queryRes?.Warnings,
62 | Fault = queryRes?.Fault,
63 | Entities = queryRes?.IntuitObjects?.Cast().ToArray()
64 | }
65 | };
66 | }
67 |
68 | public async Task> GetAsync(string id) where TEntity : IntuitEntity
69 | {
70 | var res = await _client.GetAsync(new Url(_serviceUrl).AppendPathSegment(GetEntityName(typeof(TEntity)))
71 | .AppendPathSegment(id));
72 | return new IntuitResponse
73 | {
74 | RequestId = res.requestId,
75 | Time = res.time,
76 | Status = res.status,
77 | Warnings = res.Warnings,
78 | Fault = res.Fault,
79 | Response = (TEntity?)res.IntuitObject
80 | };
81 | }
82 |
83 | public async Task> GetAsync(string id, Type entityType)
84 | {
85 | var res = await _client.GetAsync(new Url(_serviceUrl).AppendPathSegment(GetEntityName(entityType))
86 | .AppendPathSegment(id));
87 | return new IntuitResponse
88 | {
89 | RequestId = res.requestId,
90 | Time = res.time,
91 | Status = res.status,
92 | Warnings = res.Warnings,
93 | Fault = res.Fault,
94 | Response = res.IntuitObject
95 | };
96 | }
97 |
98 | public async Task GetReportAsync(string reportName, Dictionary parameters)
99 | {
100 | var url = new Url(_serviceUrl).AppendPathSegment($"reports/{reportName}");
101 | foreach (var p in parameters)
102 | url.SetQueryParam(p.Key, p.Value);
103 | return await _client.GetAsync(url);
104 | }
105 |
106 | public async Task> GetCDCAsync(DateTimeOffset changedSince, IEnumerable entityNames)
107 | {
108 | var url = new Url(_serviceUrl).AppendPathSegment($"cdc")
109 | .SetQueryParam("changedSince", changedSince)
110 | .SetQueryParam("entities", string.Join(",", entityNames));
111 | var res = await _client.GetAsync(url);
112 | return new IntuitResponse
113 | {
114 | RequestId = res.requestId,
115 | Time = res.time,
116 | Status = res.status,
117 | Warnings = res.Warnings,
118 | Fault = res.Fault,
119 |
120 | //https://help.developer.intuit.com/s/question/0D54R00007pjGeCSAU/in-the-xsd-why-is-intuitresponsecdcresponse-maxoccurs-unbounded
121 | Response = res.CDCResponse![0] //schema returns array but should be single => REPORT ERROR TO QB FORUM
122 | };
123 | }
124 |
125 | public async Task> BatchAsync(IntuitBatchRequest r)
126 | {
127 | var res = await _client.PostAsync(new Url(_serviceUrl).AppendPathSegment("batch"), r);
128 | return new IntuitResponse
129 | {
130 | RequestId = res.requestId,
131 | Time = res.time,
132 | Status = res.status,
133 | Warnings = res.Warnings,
134 | Fault = res.Fault,
135 | Response = res.BatchItemResponse
136 | };
137 | }
138 |
139 | ///
140 | public async Task> PostAsync(TEntity e, OperationEnum? operation = null, OperationEnum? include = null) where TEntity : IntuitEntity
141 | {
142 | var url = new Url(_serviceUrl).AppendPathSegment(GetEntityName(typeof(TEntity)));
143 |
144 | if (operation != null && operation != OperationEnum.Unspecified)
145 | url = url.SetQueryParam("operation", operation);
146 |
147 | if (include != null && include != OperationEnum.Unspecified)
148 | url = url.SetQueryParam("include", include);
149 |
150 | var res = await _client.PostAsync(url, e);
151 | return new IntuitResponse
152 | {
153 | RequestId = res.requestId,
154 | Time = res.time,
155 | Status = res.status,
156 | Warnings = res.Warnings,
157 | Fault = res.Fault,
158 | Response = (TEntity?)res.IntuitObject
159 | };
160 | }
161 |
162 | private async Task PostWithEntityResultAsync(TEntity e)
163 | {
164 | var url = new Url(_serviceUrl).AppendPathSegment(GetEntityName(typeof(TaxService)));
165 |
166 | return await _client.PostAsync(url, e);
167 | }
168 |
169 | ///
170 | /// Unlike other entities, TaxService is a special case where the return type is not an IntuitResponse but the entity itself.
171 | ///
172 | public async Task PostTaxServiceAsync(TaxService taxService)
173 | {
174 | return await PostWithEntityResultAsync(taxService);
175 | }
176 |
177 | ///
178 | public async Task GetInvoicePDFAsync(string invoiceId)
179 | {
180 | var url = new Url(_serviceUrl).AppendPathSegment($"/invoice/{invoiceId}/pdf");
181 | var res = await _client.SendAsync(() =>
182 | {
183 | var httpRequest = new HttpRequestMessage(HttpMethod.Get, url);
184 | httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/pdf"));
185 | return httpRequest;
186 | });
187 |
188 | return await res.Content.ReadAsStreamAsync();
189 | }
190 |
191 | private string GetEntityName(Type t)
192 | {
193 | if (t == typeof(CreditCardPaymentTxn))
194 | return "creditcardpayment";
195 | else if (t == typeof(TaxService))
196 | return "taxservice/taxcode";
197 |
198 | return t.Name.ToLowerInvariant();
199 | }
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Services/IDataService.cs:
--------------------------------------------------------------------------------
1 | using QuickBooksSharp.Entities;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Threading.Tasks;
6 |
7 | namespace QuickBooksSharp
8 | {
9 | public interface IDataService
10 | {
11 | Task> BatchAsync(IntuitBatchRequest r);
12 | Task> GetAsync(string id, Type entityType);
13 | Task> GetAsync(string id) where TEntity : IntuitEntity;
14 | Task> GetCDCAsync(DateTimeOffset changedSince, IEnumerable entityNames);
15 | Task GetReportAsync(string reportName, Dictionary parameters);
16 |
17 | ///
18 | /// Create, Update, or SparseUpdate the entity, depending on the value of the 'sparse' property
19 | ///
20 | /// QuickBooks Entity
21 | /// Entity to be sent
22 | /// Defines the operation to be executed in QuickBooks.
23 | /// Defines the "include" query parameter. For example: Required when voiding a payment
24 | ///
25 | Task> PostAsync(TEntity e, OperationEnum? operation = null, OperationEnum? include = null) where TEntity : IntuitEntity;
26 |
27 | Task>> QueryAsync(string query) where TEntity : IntuitEntity;
28 |
29 | ///
30 | /// Get an invoice as PDF
31 | /// This resource returns the specified object in the response body as an Adobe Portable Document Format (PDF) file. The resulting PDF file is formatted according to custom form styles in the company settings.
32 | /// QBO Documentation
33 | ///
34 | /// Unique identifier for this object
35 | /// This resource returns the specified object in the response body as an Adobe Portable Document Format (PDF) file. The resulting PDF file is formatted according to custom form styles in the company settings.
36 | Task GetInvoicePDFAsync(string invoiceId);
37 |
38 | Task PostTaxServiceAsync(TaxService taxService);
39 | }
40 | }
--------------------------------------------------------------------------------
/QuickBooksSharp/Webhooks/DataChangeEvent.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace QuickBooksSharp
4 | {
5 | public class DataChangeEvent
6 | {
7 | [JsonPropertyName("entities")]
8 | public EntityChange[] Entities { get; set; } = default!;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Webhooks/EntityChange.cs:
--------------------------------------------------------------------------------
1 | using QuickBooksSharp.Entities;
2 | using System;
3 | using System.Text.Json.Serialization;
4 |
5 | namespace QuickBooksSharp
6 | {
7 | ///
8 | /// Information about the entity that changed (customer, Invoice, etc.)
9 | ///
10 | public class EntityChange
11 | {
12 | ///
13 | /// The name of the entity that changed (customer, Invoice, etc.)
14 | ///
15 | [JsonPropertyName("name")]
16 | public EntityChangedName Name { get; set; }
17 | ///
18 | /// The ID of the changed entity
19 | ///
20 | [JsonPropertyName("id")]
21 | public string Id { get; set; } = default!;
22 | ///
23 | /// The type of change
24 | ///
25 | [JsonPropertyName("operation")]
26 | public OperationEnum Operation { get; set; }
27 | ///
28 | /// The latest timestamp in UTC
29 | ///
30 | [JsonPropertyName("lastUpdated")]
31 | public DateTime LastUpdated { get; set; }
32 | ///
33 | /// The ID of the deleted or merged entity (this only applies to merge events)
34 | ///
35 | [JsonPropertyName("deletedId")]
36 | public string? DeletedId { get; set; }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Webhooks/EntityChangedName.cs:
--------------------------------------------------------------------------------
1 | namespace QuickBooksSharp
2 | {
3 | ///
4 | /// Supported API entities
5 | ///
6 | /// https://developer.intuit.com/app/developer/qbo/docs/develop/webhooks#sample-implementations
7 | ///
8 | public enum EntityChangedName
9 | {
10 | Account,
11 | Bill,
12 | BillPayment,
13 | Budget,
14 | Class,
15 | CreditMemo,
16 | Currency,
17 | Customer,
18 | Department,
19 | Deposit,
20 | Employee,
21 | Estimate,
22 | Invoice,
23 | Item,
24 | JournalCode,
25 | JournalEntry,
26 | Payment,
27 | PaymentMethod,
28 | Preferences,
29 | Purchase,
30 | PurchaseOrder,
31 | RefundReceipt,
32 | SalesReceipt,
33 | TaxAgency,
34 | Term,
35 | TimeActivity,
36 | Transfer,
37 | Vendor,
38 | VendorCredit
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Webhooks/EventNotification.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace QuickBooksSharp
4 | {
5 | public class EventNotification
6 | {
7 | [JsonPropertyName("realmId")]
8 | public string RealmId { get; set; } = default!;
9 | [JsonPropertyName("dataChangeEvent")]
10 | public DataChangeEvent DataChangeEvent { get; set; } = default!;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/QuickBooksSharp/Webhooks/WebhookEvent.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 |
5 | namespace QuickBooksSharp
6 | {
7 | ///
8 | /// Webhooks are designed to handle updates for multiple QuickBooks Online companies (specified by the realm ID).
9 | /// However, each notification is tied to a single realm ID.If your app is connected to multiple QuickBooks Online companies, you’ll get individual webhook notifications for each company (i.e.one eventNotification per realm ID).
10 | ///
11 | /// Documentation: https://developer.intuit.com/app/developer/qbo/docs/develop/webhooks
12 | ///
13 | ///
14 | /// There are no Intuit-imposed limits to payload size or number of events. Individual server architectures may impose their own limits (2MB is a common default size limit). Assume this limit is imposed by your server unless you know otherwise.
15 | ///
16 | public class WebhookEvent
17 | {
18 | [JsonPropertyName("eventNotifications")]
19 | public EventNotification[] EventNotifications { get; set; } = default!;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # QuickBooksSharp
2 |
3 | Modern, async first, .NET client for QuickBooks Online Accounting API
4 |
5 | This project was created because the [official .NET client](https://github.com/intuit/QuickBooks-V3-DotNET-SDK) provided by Intuit is hard to work with and based on outdated .NET patterns.
6 |
7 | https://developer.intuit.com/app/developer/qbo/docs/develop
8 |
9 | API version: 3.75
10 |
11 | 
12 |
13 | https://www.nuget.org/packages/QuickBooksSharp/
14 |
15 | `Install-Package QuickBooksSharp`
16 |
17 | ## OAuth authentication
18 |
19 | ### Generate URL to redirect user for approval of connection:
20 | ```csharp
21 | var authService = new AuthenticationService();
22 | var scopes = new[] { "com.intuit.quickbooks.accounting" };
23 | string redirectUrl = "https://myapp.com/quickbooks/authresult";
24 | string state = Guid.NewGuid().ToString();
25 | string authUrl = authService.GenerateAuthorizationPromptUrl(clientId, scopes, redirectUrl, state);
26 | // Redirect the user to authUrl so that they can approve the connection
27 | ```
28 |
29 | ### Exchange code for token
30 | ```csharp
31 | [HttpGet]
32 | public async Task AuthResult(string code, long realmId, string state)
33 | {
34 | //validate state parameter
35 | var authService = new AuthenticationService();
36 | string clientId = //get from config
37 | string clientSecret = //get from config
38 | var result = await authService.GetOAuthTokenAsync(clientId, clientSecret, code, redirectUrl);
39 | //persit access token and refresh token
40 | ...
41 | }
42 | ```
43 |
44 | ### Refresh token
45 | ```csharp
46 | var authService = new AuthenticationService();
47 | var result = await authService.RefreshOAuthTokenAsync(clientId, clientSecret, refreshToken);
48 | //persit access token and refresh token
49 | ```
50 |
51 | ### Get User Info
52 | ```csharp
53 | var authService = new AuthenticationService();
54 | var userInfo = await authService.GetUserInfo(accessToken, useSandbox: true);
55 | //persit access token and refresh token
56 | ```
57 |
58 | ## Instantiating the DataService
59 | ```csharp
60 | var dataService = new DataService(accessToken, realmId, useSandbox: true);
61 | ```
62 |
63 | ## Creating / Updating entities
64 | ```csharp
65 | var result = await dataService.PostAsync(new Customer
66 | {
67 | DisplayName = "Chandler Bing",
68 | Suffix = "Jr",
69 | Title = "Mr",
70 | MiddleName = "Muriel",
71 | FamilyName = "Bing",
72 | GivenName = "Chandler",
73 | });
74 | //result.Response is of type Customer
75 | var customer = result.Response;
76 |
77 | //Sparse update some properties
78 | result = await dataService.PostAsync(new Customer
79 | {
80 | Id = customer.Id,
81 | SyncToken = customer.SyncToken,
82 | GivenName = "Ross",
83 | sparse = true
84 | });
85 |
86 | //Update all properties
87 | customer = result.Response;
88 | customer.FamilyName = "Geller";
89 | customer.sparse = false;
90 | result = await dataService.PostAsync(customer);
91 | ```
92 |
93 | ## Delete entities
94 | ```csharp
95 | var result = await dataService.PostAsync(new Invoice { Id = "123", SyncToken = syncToken }, OperationEnum.delete);
96 | ```
97 |
98 | ## Querying entities
99 | ```csharp
100 | var result = await dataService.QueryAsync("SELECT * FROM Customer")
101 | //res.Response.Entities is of type Customer[]
102 | var customers = res.Response.Entities;
103 | ```
104 |
105 | ```csharp
106 | var result = await dataService.QueryCountAsync("SELECT COUNT(*) FROM Customer");
107 | var count = res.Response.TotalCount;
108 | ```
109 |
110 | ## Querying reports
111 | ```csharp
112 | var report = await dataService.GetReportAsync("ProfitAndLoss", new()
113 | {
114 | { "accounting_method", "Accrual" },
115 | { "date_macro", "Last Fiscal Year" }
116 | });
117 | string reportName = report.Header.ReportName;
118 | ```
119 |
120 | ## Change Data Capture (CDC)
121 | ```csharp
122 | var result = await dataService.GetCDCAsync(DateTimeOffset.UtcNow.AddDays(-10), "Customer,Invoice");
123 | var queryResponses = result.Response.QueryResponse; //type QueryResponse[]
124 | var customers = queryResponses[0].IntuitObjects.Cast();
125 | var invoices = queryResponses[1].IntuitObjects.Cast();
126 | ```
127 |
128 | ## Batch
129 | ```csharp
130 | //Delete 30 bills in a batch
131 | var bills = (await dataService.QueryAsync("SELECT * FROM Bill MAXRESULTS 30")).Response.Entities;
132 | var response = await dataService.BatchAsync(new IntuitBatchRequest
133 | {
134 | BatchItemRequest = bills.Select(b => new BatchItemRequest
135 | {
136 | bId = Guid.NewGuid().ToString(),
137 | operation = OperationEnum.delete,
138 | Bill = new Bill
139 | {
140 | Id = b.Id,
141 | SyncToken = b.SyncToken
142 | }
143 | }).ToArray()
144 | });
145 |
146 | //Issue multiple queries in a batch
147 | var response = await dataService.BatchAsync(new IntuitBatchRequest
148 | {
149 | BatchItemRequest = new[]
150 | {
151 | new BatchItemRequest
152 | {
153 | bId = Guid.NewGuid().ToString(),
154 | Query = "SELECT * FROM Bill MAXRESULTS 30",
155 | },
156 | new BatchItemRequest
157 | {
158 | bId = Guid.NewGuid().ToString(),
159 | Query = "SELECT * FROM Invoice MAXRESULTS 30",
160 | }
161 | }
162 | });
163 | ```
164 |
165 | ## Verifying webhooks
166 | ```csharp
167 | [HttpPost]
168 | [IgnoreAntiforgeryToken]
169 | [AllowAnonymous]
170 | public async Task Webhook()
171 | {
172 | string signature = Request.Headers["intuit-signature"].ToString();
173 | string webhookVerifierToken = //get from config
174 | string requestBodyJSON = await base.ReadBodyToEndAsync();
175 | if (!Helper.IsAuthenticWebhook(signature, webhookVerifierToken, requestBodyJSON))
176 | return BadRequest();
177 | //return HTTP error status
178 |
179 | //Process webhook
180 | WebhookEvent notification = JsonSerializer.Deserialize(requestBodyJSON, QuickBooksHttpClient.JsonSerializerOptions);
181 | }
182 | ```
183 |
184 | ## Download Invoice PDF
185 | ```csharp
186 | var invoiceId = "1023";
187 | var invoidePdfStream = await dataService.GetInvoicePDF(invoiceId);
188 | ```
189 |
--------------------------------------------------------------------------------